Skip to content
dinhostorkPublic

About

Pulso is an experimental mobile platform that organizes multiple news publications into shared events and transforms public discussion into structured Perspectives, recurring arguments and counterpoints.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

62 Commits

Folders and files

Repository files navigation

Pulso

Multi-source news, structured public opinion, and personalized discovery without personalizing the facts.

Pulso is an experimental mobile platform that organizes multiple news publications into shared events and transforms public discussion into structured Perspectives, recurring arguments and counterpoints.

Instead of treating each publication as a separate feed item or organizing discussion as an endless comment section, Pulso is built around two ideas:

  1. Stories consolidate coverage of the same event from multiple sources.
  2. The Pulse structures how participating users position themselves and why.

News is the entry point. Understanding collective opinion is the product.


Product preview

Pulso feed wireframe The Pulse wireframe Perspective wireframe

Low-fidelity wireframes. Visual design is still evolving.

More screens and UX decisions are documented in docs/ux.


Why Pulso?

Digital news consumption has a few recurring problems:

  • several publications repeat coverage of the same event;
  • headlines often provide little context;
  • comparing multiple sources requires additional effort;
  • factual information and opinion are frequently mixed;
  • public discussion becomes fragmented across comment sections;
  • popular reactions are easier to find than recurring arguments;
  • relevant counterpoints can disappear under engagement-driven ranking;
  • personalized feeds can narrow discovery without making that behavior clear.

Pulso approaches these problems by organizing information around events, preserving source access and structuring community discussion semantically.


How it works

flowchart LR
    Sources["News Sources"]
    Articles["Articles"]
    Story["Story"]
    Feed["Personalized Feed"]

    Opinions["User Opinions"]
    Perspectives["Perspectives"]
    Pulse["The Pulse"]

    Sources --> Articles
    Articles --> Story

    Story --> Feed
    Story --> Opinions

    Opinions --> Perspectives
    Opinions --> Pulse

    Perspectives --> Pulse
Loading

The platform has three main logical engines:

flowchart LR
    News["News Engine"]
    Opinion["Opinion Engine"]
    Recommendation["Recommendation Engine"]

    News --> Stories["Stories"]
    Stories --> Opinion
    Stories --> Recommendation

    Opinion --> Discussion["Perspectives + Pulse"]
    Recommendation --> Feed["Personalized Feed"]
Loading

These engines are logical domain components of the same application. They are not independent microservices.


News Engine

The News Engine connects publications to event-level Stories. News Core (v0.2) ingests, normalizes and deduplicates publications into Articles. The Story Engine (v0.3) embeds each Article, matches it deterministically into a Story, and keeps each Story's Topics, Entities and source-grounded synthesis coherent with its members. See the News Core architecture and the Story Engine architecture.

flowchart LR
    Sources["Sources"]
    Raw["Raw Articles"]
    Normalize["Normalization"]
    Dedup["Deduplication"]
    Embeddings["Embeddings"]
    Clustering["Story Matching"]
    Story["Story"]
    Enrichment["Topics + Entities + Synthesis"]

    Sources --> Raw
    Raw --> Normalize
    Normalize --> Dedup
    Dedup --> Embeddings
    Embeddings --> Clustering
    Clustering --> Story
    Story --> Enrichment
Loading

A fundamental domain rule is:

Article != Story

An Article represents one publication from one source.

A Story represents the event described by one or more Articles.

Original sources remain accessible and support the factual layer of the Story.

See ADR-0003.


Opinion Engine

Pulso does not treat public discussion as a traditional chronological comment section.

Users publish an Opinion containing their current position and main argument.

Semantically related Opinions can then be organized into Perspectives.

flowchart LR
    Opinion["Opinion"]
    Embedding["Embedding"]
    Clustering["Semantic Grouping"]
    Perspective["Perspective"]
    Arguments["Recurring Arguments"]
    Counterpoints["Counterpoints"]
    Pulse["The Pulse"]

    Opinion --> Embedding
    Embedding --> Clustering
    Clustering --> Perspective

    Perspective --> Arguments
    Perspective --> Counterpoints

    Opinion --> Pulse
Loading

Another fundamental rule is:

Opinion != Perspective

An Opinion is human-authored content.

A Perspective is a system-derived representation of recurring reasoning across multiple Opinions.

Individual Opinions remain available for transparency and nuance.

See ADR-0006.


The Pulse

The Pulse provides a structured view of how participating users currently position themselves around a Story.

Initial positions are:

SUPPORT
OPPOSE
PARTIAL
UNDECIDED

The distribution follows a simple invariant:

One user, one active position, per Story.

Replies, Perspective memberships and Represents me interactions do not create additional votes.

flowchart LR
    User["Unique User"]
    Position["Current Position"]
    Aggregate["Aggregate by Story"]
    Pulse["The Pulse"]

    User --> Position
    Position --> Aggregate
    Aggregate --> Pulse
Loading

The Pulse describes participating Pulso users. It is not a statistically representative public opinion poll.

See ADR-0007.


Represents me

Pulso avoids using a generic Like as the primary signal for arguments.

Represents me means:

This argument adequately represents my view on this point.

A user may identify with several Perspectives while still contributing only one active position to the Pulse.

Position
    ≠
Perspective
    ≠
Representation

AI principles

AI and NLP are used to help:

  • generate embeddings;
  • identify semantic similarity;
  • cluster Articles into Stories;
  • cluster Opinions into Perspectives;
  • extract entities and topics;
  • summarize source-backed information;
  • synthesize recurring arguments;
  • identify possible counterpoints.

The core rule is:

Sources support the information. AI organizes, relates and summarizes it.

AI-generated output is derived data.

It is not an independent factual source.

flowchart LR
    Sources["Source Articles"]
    AI["AI / NLP"]
    Story["Story Synthesis"]

    Opinions["User Opinions"]
    AI2["AI / NLP"]
    Perspective["Perspective Synthesis"]

    Sources --> AI
    AI --> Story

    Opinions --> AI2
    AI2 --> Perspective
Loading

Original Articles and Opinions remain authoritative inputs.

See ADR-0004.


Recommendation principles

Pulso personalizes discovery, not the factual representation of an event.

Different users may receive different Story rankings:

User A feed != User B feed

but when they open the same Story:

Story X for User A = Story X for User B

The Recommendation Engine may influence:

  • which Stories enter the feed;
  • Story ordering;
  • exploration;
  • topic diversity.

It must not rewrite:

  • Story facts;
  • Story summaries according to user beliefs;
  • source evidence;
  • Pulse percentages;
  • community Perspectives.
flowchart LR
    User["User Interests + Behavior"]
    Stories["Stories"]
    Recommendation["Recommendation Engine"]
    Feed["Personalized Feed"]

    User --> Recommendation
    Stories --> Recommendation
    Recommendation --> Feed
Loading

See ADR-0008.


Architecture

Pulso starts as a modular monolith with background workers.

flowchart TB
    Mobile["React Native / Expo"]

    subgraph Backend["Pulso Backend"]
        API["Django / DRF API"]

        subgraph Modules["Application Modules"]
            News["News Engine"]
            Opinion["Opinion Engine"]
            Recommendation["Recommendation Engine"]
        end

        Worker["Celery Workers"]
        Scheduler["Celery Beat"]
    end

    DB[("PostgreSQL + pgvector")]
    Redis[("Redis")]

    Mobile --> API

    API --> Modules
    Worker --> Modules

    Scheduler --> Redis
    API --> Redis
    Redis --> Worker

    Modules --> DB
Loading

The API, workers and scheduler may execute as separate runtime processes while sharing the same application codebase and domain modules.

Heavy or retryable processing is moved outside the HTTP request lifecycle.

See:


Data

PostgreSQL is the primary persistent datastore.

pgvector provides vector storage and similarity search without requiring a separate vector database during the initial architecture.

Vector use cases include:

  • Article → Story similarity;
  • Opinion → Perspective similarity;
  • Story recommendation;
  • user-interest representations.
flowchart LR
    News["News Engine"]
    Opinion["Opinion Engine"]
    Recommendation["Recommendation Engine"]

    DB[("PostgreSQL")]
    Vector["pgvector"]

    News --> DB
    Opinion --> DB
    Recommendation --> DB

    DB --- Vector
Loading

See ADR-0002.


Technology direction

Mobile

  • React Native
  • Expo
  • TypeScript

Backend

  • Python
  • Django
  • Django REST Framework

Data

  • PostgreSQL
  • pgvector
  • Redis

Background processing

  • Celery
  • Celery Beat

AI / NLP

The architecture is designed to support local and external implementations through explicit application boundaries.

The v0.3 Story Engine uses an optional local open-source embedding model (BAAI/bge-small-en-v1.5 via fastembed) and deterministic, offline extraction and extractive synthesis; no hosted AI API is required.

Infrastructure

  • Docker
  • Docker Compose
  • GitHub Actions

The project follows a free-first approach: local development should not require paid infrastructure or mandatory commercial AI APIs.


Architecture decisions

Major architectural and domain decisions are documented as ADRs.

ADR Decision
ADR-0001 Modular Monolith with Background Workers
ADR-0002 PostgreSQL with pgvector
ADR-0003 Article != Story
ADR-0004 AI Is Not a Source of Truth
ADR-0005 Asynchronous Processing with Celery
ADR-0006 Opinion != Perspective
ADR-0007 Pulse Counts Unique Users
ADR-0008 Recommendation Personalizes Discovery, Not Truth
ADR-0009 JWT Bearer Authentication for the Mobile API
ADR-0010 Article Identity and Deduplication
ADR-0011 Reading Owns Durable User References to Stories
ADR-0012 FeedImpression Is a Qualified Client-Reported Exposure

ADRs describe why these decisions were made, which alternatives were considered and the conditions under which they may be revisited.


Continuous integration

Every pull request and every push to master runs three GitHub Actions checks, defined in .github/workflows/ci.yml and reusing the same commands documented in backend/README.md and mobile/README.md:

Check name What it runs
backend Ruff lint/format, Django system checks (dev and isolated test settings), migration-drift check, and the full pytest suite against a real disposable PostgreSQL/pgvector and Redis
worker-smoke Compose configuration validation, then the real-broker Celery smoke check (pytest -m celery_smoke) against a separately running worker process, not eager/local execution
mobile expo-doctor, ESLint, Prettier, TypeScript, the Jest suite and a web export route check, from the committed lockfile

These are the exact names to reference from Settings → Branches → Branch protection rules → Require status checks to pass when enabling required checks on master — a repository-settings change this workflow itself never makes. All three run with permissions: contents: read, use only disposable local services and the repository's own public example configuration (no repository secrets, no paid service), and are bounded by a per-job timeout-minutes plus each step's own internal bounds (Compose --wait-timeout, the smoke test's own per-call result timeout).


Local development

docs/development.md is the single walkthrough for running the whole Foundation locally from a clean checkout — backend, PostgreSQL/pgvector, Redis, the Celery worker, optional Beat, the mobile shell, quality/test commands, troubleshooting, and the issue → branch → PR contribution workflow. backend/README.md and mobile/README.md remain the detailed per-stack references it links out to.


UX documentation

The current interaction model and low-fidelity wireframes are available in:

docs/ux/README.md

The main exploration flow is:

Feed
  ↓
Story
  ↓
The Pulse
  ↓
Perspective
  ↓
Arguments
  ↓
Counterpoint
  ↓
Individual Opinions

Opinion publication follows:

Story
  ↓
Give my Opinion
  ↓
Position
  ↓
Argument
  ↓
Publish

Project status

Current stage: Mobile Feed implemented; Opinion Core next.

The Foundation, News Core, Story Engine and Mobile Feed are implemented and tested. Story grouping quality is measured on a repository-owned synthetic corpus, not on production data. Mobile Feed was accepted on a physical Android device; iOS was not validated and is recorded as a platform exception in the Phase 3 acceptance record, together with its known limitations. Opinion Core is the next milestone; product and UX work continues to evolve.

The wireframes represent the current product hypothesis and are not final visual design.

The repository should not be interpreted as a production-ready application at this stage.


Roadmap

Phase 0 — Foundation (v0.1.0)

  • repository structure;
  • backend bootstrap;
  • mobile bootstrap;
  • PostgreSQL + pgvector;
  • Redis;
  • Docker Compose;
  • authentication foundation;
  • CI;
  • linting and tests.

Phase 1 — News Core (v0.2.0)

  • Source ingestion;
  • RSS/Atom and JSON Feed adapters;
  • extensible ingestion adapter boundary;
  • RawArticle persistence;
  • Article normalization;
  • deduplication.

Phase 2 — Story Engine (v0.3.0)

Implemented; see the Story Engine architecture.

  • embeddings;
  • semantic similarity;
  • Article → Story grouping;
  • Topics and Entities;
  • source-grounded summaries.

Phase 3 — Mobile Feed (v0.4.0)

Implemented; see the Mobile Feed architecture and the acceptance record.

  • Story feed;
  • Story details;
  • source access;
  • bookmarks;
  • FeedImpressions.

Phase 4 — Opinion Core

  • positions;
  • Opinions;
  • replies;
  • moderation;
  • Represents me.

Phase 5 — Opinion Engine

  • Opinion embeddings;
  • semantic Perspective grouping;
  • recurring arguments;
  • counterpoints;
  • Pulse aggregation.

Phase 6 — Recommendation

  • interest profile;
  • behavioral signals;
  • semantic ranking;
  • exploration;
  • diversity.

Phase 7 — Pulse Evolution

  • position history;
  • Pulse snapshots;
  • evolution views.

Phase 8 — Production / Demo

  • observability;
  • deployment;
  • performance validation;
  • public demo.

MVP

The MVP should allow a user to:

  1. create an account and select interests;
  2. receive a feed organized by multi-source Stories;
  3. understand what happened and inspect original sources;
  4. open the Pulse for a Story;
  5. explore Perspectives and counterpoints;
  6. publish a position and argument;
  7. see Opinions organized into recurring Perspectives;
  8. use Represents me;
  9. receive progressively personalized Story discovery.

The automated path should support:

Article
   ↓
Story
   ↓
Opinion
   ↓
Perspective
   ↓
Pulse

without requiring mandatory manual editorial intervention.


Core invariants

Article != Story

Opinion != Perspective

Position != Perspective

AI != Source

One user = one active position per Story

"Represents me" != vote

Recommendation != factual personalization

Their status after Phase 3 is reviewed against the code in the acceptance record.

These constraints are intentional parts of the product and domain model, not implementation accidents.


License

License information will be added before the first public release.

About

Pulso is an experimental mobile platform that organizes multiple news publications into shared events and transforms public discussion into structured Perspectives, recurring arguments and counterpoints.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages