Files
stonks-oracle/.kiro/specs/competitive-historical-patterns/design.md
T

29 KiB
Raw Blame History

Competitive Intelligence & Historical Pattern Matching Layer — Design

Overview

This design adds a third signal layer to the Stonks Oracle aggregation engine: competitive intelligence and historical pattern matching. The layer mines existing PostgreSQL data — document_impact_records, trend_windows, and document_company_mentions — to identify how similar catalyst types resolved historically for a company and its competitors, then feeds pattern-based signals into the aggregation engine alongside company-specific (layer 1) and macro (layer 2) signals.

The design follows the same integration pattern as the macro interpolation layer: new modules within existing services, a runtime toggle in risk_configs, and the same WeightedSignal abstraction for aggregation. No new Kubernetes deployments are required.

Design Rationale

  • Mine existing data, no new ingestion: All pattern signals derive from data already in PostgreSQL — document intelligence, impact records, and trend windows. No new external data sources or ingestion pipelines.
  • Reuse existing scoring pipeline: Pattern signals convert to WeightedSignal objects using the same compute_signal_weight function, ensuring consistent recency decay, confidence gating, and contradiction detection.
  • Parallel to macro layer: The competitive layer toggle, suppression logic, and aggregation integration mirror the macro layer's architecture for consistency.
  • Safety-first: Low-confidence patterns (< 0.3) are excluded, pattern-only trend shifts are forced to informational mode, and the entire layer is independently toggleable.
  • Competitor relationships as first-class entities: Both operator-defined and auto-inferred relationships, with strength scores that gate signal propagation.

Architecture

The competitive intelligence layer adds five logical components within existing services:

flowchart TD
    subgraph SymReg["Symbol Registry (existing)"]
        CR[Competitor Registry]
        AI[Auto-Inference Engine]
    end

    subgraph Aggregation["Aggregation Service (existing)"]
        PM[Pattern Matcher]
        SPE[Signal Propagation Engine]
        AE[Aggregation Engine]
    end

    subgraph Recommendation["Recommendation Service (existing)"]
        PS[Pattern-Only Suppression]
    end

    subgraph LakePublisher["Lake Publisher (existing)"]
        LP[Competitive Fact Publisher]
    end

    subgraph QueryAPI["Query API (existing)"]
        PA[Pattern API Endpoints]
        CT[Competitive Toggle Endpoint]
    end

    subgraph Dashboard["Dashboard (existing)"]
        CP[Competitors Panel]
        HP[Historical Patterns Panel]
        CS[Competitive Signals Panel]
        DT[Decision Timeline]
    end

    CR -->|competitor relationships| SPE
    AI -->|inferred relationships| CR
    PM -->|historical patterns| SPE
    PM -->|self-company patterns| AE
    SPE -->|competitive signals| AE
    AE -->|trend summaries| PS
    SPE -->|signal records| LP
    CT -->|toggle state| AE
    PA --> CP
    PA --> HP
    PA --> CS
    PA --> DT

Data Flow

  1. Competitor Management: Operators define competitor relationships via the Symbol Registry API, or trigger auto-inference from sector/industry and document co-mentions. Relationships are stored in competitor_relationships.

  2. Pattern Mining: When the aggregation engine runs for a ticker, the Pattern Matcher queries document_impact_records joined with trend_windows to find historical instances of the same catalyst type. It computes outcome statistics (bullish_pct, bearish_pct, avg_strength) and a pattern_confidence score.

  3. Signal Propagation: The Signal Propagation Engine looks up the ticker's competitors, queries the Pattern Matcher for cross-company historical patterns, and produces competitive_signal_records weighted by relationship strength × pattern confidence × source impact score.

  4. Aggregation: Pattern signals (both self-company and competitive) are converted to WeightedSignal objects and merged into the existing signal list. The competitive layer toggle is checked from risk_configs at the start of each cycle.

  5. Recommendation Safety: Pattern-only trend shifts (no supporting company-specific or macro signals) are forced to informational mode with a pattern-only caveat.

  6. Lake Publication: Competitor relationships and competitive signal facts are published as partitioned Parquet datasets.

Components and Interfaces

Competitor Registry

Location: services/symbol_registry/competitors.py (new module, registered as a FastAPI router in app.py)

Manages competitor relationships with CRUD operations and audit logging.

class CompetitorRelationshipCreate(BaseModel):
    company_b_id: str
    relationship_type: str  # direct_rival | same_sector | overlapping_products | supply_chain_adjacent
    strength: float  # [0, 1]
    bidirectional: bool = True
    source: str = "manual"  # manual | inferred

class CompetitorRelationship(BaseModel):
    id: str
    company_a_id: str
    company_b_id: str
    relationship_type: str
    strength: float
    bidirectional: bool
    source: str
    active: bool
    created_at: datetime
    updated_at: datetime

API Endpoints (on Symbol Registry):

  • POST /companies/{company_id}/competitors — create relationship
  • GET /companies/{company_id}/competitors — list relationships (ordered by strength desc)
  • PUT /companies/{company_id}/competitors/{relationship_id} — update relationship
  • DELETE /companies/{company_id}/competitors/{relationship_id} — soft-delete (set active=false)
  • POST /companies/{company_id}/competitors/infer — trigger auto-inference

Auto-Inference Logic (services/symbol_registry/competitor_inference.py):

  1. Query companies sharing the same sector and industry
  2. Rank candidates by co-mention frequency in document_company_mentions
  3. Compute strength = 0.3 * sector_match + 0.7 * normalized_co_mention_count
  4. Upsert relationships with source='inferred', refreshing strength on re-inference
  5. Return candidate list for operator review

Pattern Matcher

Location: services/aggregation/pattern_matcher.py

Queries historical data to find how similar catalyst types resolved for a company or its competitors.

@dataclass
class HistoricalPattern:
    source_ticker: str          # company that received the original catalyst
    target_ticker: str          # company being evaluated (same as source for self-patterns)
    catalyst_type: str
    time_horizon: str           # 1d | 7d | 30d
    sample_count: int
    bullish_pct: float          # [0, 1]
    bearish_pct: float          # [0, 1]
    avg_strength: float         # [0, 1]
    avg_time_to_resolution: float  # days
    pattern_confidence: float   # [0, 1]
    data_start: datetime
    data_end: datetime
    tier: str                   # major_corporate_decision | routine_signal
    insufficient_data: bool     # True when sample_count < 3

Core Functions:

  • find_self_patterns(pool, ticker, catalyst_type, horizons) -> list[HistoricalPattern]
  • find_cross_company_patterns(pool, source_ticker, target_ticker, catalyst_type, horizons) -> list[HistoricalPattern]
  • compute_pattern_confidence(sample_count, outcome_consistency, data_recency_days) -> float
  • classify_catalyst_tier(catalyst_type) -> str — returns major_corporate_decision or routine_signal

Pattern Confidence Formula:

sample_factor = min(sample_count / 20, 1.0)  # diminishing returns above 20
consistency = max(bullish_pct, bearish_pct)   # how uniform outcomes are
recency_factor = 1.0 if newest_within_90d else 0.7 if newest_within_180d else 0.4
confidence = sample_factor * 0.4 + consistency * 0.4 + recency_factor * 0.2

Insufficient Data: When sample_count < 3, confidence is capped at 0.25 and insufficient_data = True.

Staleness Decay (Req 9.2): When no instances exist in the last 90 days and all data is older than 180 days, a 0.5 decay penalty is applied to confidence.

Catalyst Tier Classification (Req 11.1):

  • major_corporate_decision: catalyst types m_and_a, legal, restructuring, leadership_change, strategic_pivot, buyback, dividend_change
  • routine_signal: all other catalyst types
  • Major decisions use 365-day lookback; routine signals use 180-day lookback
  • Major decisions receive a 1.3× base weight multiplier on pattern_confidence

Historical Query: Only considers document_impact_records linked to document_intelligence with validation_status = 'valid' and documents with status != 'rejected'.

Signal Propagation Engine

Location: services/aggregation/signal_propagation.py

Evaluates incoming document intelligence, identifies competitors, queries historical patterns, and produces competitive signals.

@dataclass
class CompetitiveSignalRecord:
    source_document_id: str
    source_ticker: str
    target_ticker: str
    catalyst_type: str
    pattern_confidence: float
    signal_direction: str       # bullish | bearish
    signal_strength: float      # [0, 1]
    relationship_strength: float
    computed_at: datetime

Core Functions:

  • propagate_signals(pool, ticker, catalyst_type, impact_score, document_id, config) -> list[CompetitiveSignalRecord]
  • build_pattern_weighted_signals(patterns, competitive_signals, reference_time, window, config) -> list[WeightedSignal]

Signal Weighting:

signal_strength = pattern.avg_strength * relationship.strength * pattern.pattern_confidence * source_impact_score
signal_direction = "bullish" if pattern.bullish_pct > pattern.bearish_pct else "bearish"

Propagation Threshold (Req 4.5): Skip propagation when relationship.strength < 0.2 (configurable).

Confidence Threshold (Req 9.1): Exclude patterns with pattern_confidence < 0.3 (configurable).

Aggregation Engine Extensions

Location: Modified services/aggregation/worker.py

The existing aggregate_company_window function is extended to:

  1. Check the competitive layer toggle from risk_configs (same pattern as macro toggle)
  2. Query self-company historical patterns for active catalyst types in the window
  3. Query competitive signals targeting this ticker
  4. Convert pattern/competitive signals to WeightedSignal objects
  5. Merge with company-specific and macro signals before computing the trend summary

New config field on AggregationConfig:

competitive_signal_weight: float = 0.2  # relative weight of pattern signals
competitive_enabled: bool = True         # runtime toggle state

Pattern signal conversion: Each pattern signal is converted to a WeightedSignal using:

  • document_id = source document that triggered the pattern lookup (for evidence tracing)
  • sentiment_value = +1.0 if pattern direction is bullish, -1.0 if bearish
  • impact_score = signal_strength * competitive_signal_weight
  • Recency decay uses the source document's publication time
  • Confidence gating uses pattern_confidence as the extraction confidence

No-degradation guarantee (Req 5.5): When no patterns or competitive signals exist, the aggregation produces identical output to the two-layer engine.

Pattern-Only Suppression

Location: Extended services/recommendation/suppression.py

New suppression check mirroring evaluate_macro_only_suppression:

PATTERN_ONLY_CAVEAT = (
    "[Pattern-only signal] This trend direction is driven solely by historical "
    "pattern and competitive signals with no supporting company-specific or macro "
    "evidence. Recommendation is informational only."
)

def evaluate_pattern_only_suppression(
    summary: TrendSummary,
    pattern_signal_count: int,
    company_signal_count: int,
    macro_signal_count: int,
) -> bool

New SuppressionReason enum value: PATTERN_ONLY_SIGNAL = "pattern_only_signal"

Query API Extensions

Location: Extended services/api/app.py

New endpoints:

  • GET /api/patterns/{ticker} — historical patterns for a company, filterable by catalyst_type and time_horizon
  • GET /api/patterns/{ticker}/competitors — cross-company patterns showing how this company's catalysts affected competitors
  • GET /api/patterns/{ticker}/competitive-signals — recent competitive signals targeting this company
  • GET /api/patterns/{ticker}/decisions — major corporate decision history with trend outcomes
  • GET /api/admin/competitive/status — competitive layer enabled/disabled state
  • PUT /api/admin/competitive/toggle — toggle competitive layer on/off

Dashboard Extensions

Location: Extended frontend/src/

New panels on Company Detail page (new tabs alongside existing sources/aliases/macro):

  • Competitors tab: Active competitor relationships with ticker, relationship_type, strength, source
  • Historical Patterns tab: Recent patterns for the company — catalyst_type, outcome distribution, sample_count, confidence
  • Competitive Signals tab: Incoming competitive signals — source ticker, catalyst_type, direction, strength
  • Decisions tab: Corporate decision timeline — major events with catalyst type, date, summary, trend outcome

Trend detail page extensions:

  • Visual distinction for pattern-based and competitive signal evidence (badge/icon differentiation)
  • Click-through on competitive signals showing full signal detail

Trading Controls page:

  • Competitive layer toggle alongside existing macro toggle, with confirmation dialog

Data Models

New PostgreSQL Tables (Migration 017)

competitor_relationships

CREATE TABLE competitor_relationships (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    company_a_id UUID NOT NULL REFERENCES companies(id),
    company_b_id UUID NOT NULL REFERENCES companies(id),
    relationship_type VARCHAR(30) NOT NULL,
    strength FLOAT NOT NULL DEFAULT 0.5,
    bidirectional BOOLEAN NOT NULL DEFAULT TRUE,
    source VARCHAR(20) NOT NULL DEFAULT 'manual',
    active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT chk_relationship_type CHECK (
        relationship_type IN ('direct_rival', 'same_sector', 'overlapping_products', 'supply_chain_adjacent')
    ),
    CONSTRAINT chk_strength CHECK (strength >= 0 AND strength <= 1),
    CONSTRAINT chk_source CHECK (source IN ('manual', 'inferred')),
    CONSTRAINT chk_different_companies CHECK (company_a_id != company_b_id)
);

CREATE INDEX idx_competitor_rel_company_a ON competitor_relationships(company_a_id) WHERE active = TRUE;
CREATE INDEX idx_competitor_rel_company_b ON competitor_relationships(company_b_id) WHERE active = TRUE;
CREATE UNIQUE INDEX idx_competitor_rel_unique_pair ON competitor_relationships(
    LEAST(company_a_id, company_b_id), GREATEST(company_a_id, company_b_id)
) WHERE active = TRUE;

competitive_signal_records

CREATE TABLE competitive_signal_records (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    source_document_id UUID REFERENCES documents(id),
    source_ticker VARCHAR(20) NOT NULL,
    target_ticker VARCHAR(20) NOT NULL,
    catalyst_type VARCHAR(50) NOT NULL,
    pattern_confidence FLOAT NOT NULL,
    signal_direction VARCHAR(20) NOT NULL,
    signal_strength FLOAT NOT NULL,
    relationship_strength FLOAT NOT NULL,
    computed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_competitive_signals_target ON competitive_signal_records(target_ticker, computed_at DESC);
CREATE INDEX idx_competitive_signals_source ON competitive_signal_records(source_ticker, computed_at DESC);

New Pydantic Schemas

Added to services/shared/schemas.py:

class RelationshipType(str, Enum):
    DIRECT_RIVAL = "direct_rival"
    SAME_SECTOR = "same_sector"
    OVERLAPPING_PRODUCTS = "overlapping_products"
    SUPPLY_CHAIN_ADJACENT = "supply_chain_adjacent"

class CatalystTier(str, Enum):
    MAJOR_CORPORATE_DECISION = "major_corporate_decision"
    ROUTINE_SIGNAL = "routine_signal"

# Major corporate decision catalyst types (Req 11.1)
MAJOR_DECISION_CATALYSTS = frozenset({
    "m_and_a", "legal", "restructuring", "leadership_change",
    "strategic_pivot", "buyback", "dividend_change",
})

New CompetitiveConfig in services/shared/config.py

@dataclass
class CompetitiveConfig:
    competitive_signal_weight: float = 0.2
    competitive_enabled: bool = True
    pattern_confidence_threshold: float = 0.3
    propagation_strength_threshold: float = 0.2
    routine_lookback_days: int = 180
    major_decision_lookback_days: int = 365
    major_decision_weight_multiplier: float = 1.3
    staleness_window_days: int = 180
    staleness_recent_days: int = 90
    staleness_decay_penalty: float = 0.5
    min_pattern_samples: int = 3

Analytical Lake Datasets

New fact tables published to MinIO under stonks-lakehouse/:

  • lake.competitor_relationships — partitioned by dt, columns: id, company_a_id, company_b_id, relationship_type, strength, bidirectional, source, active, created_at
  • lake.competitive_signals — partitioned by dt and target_ticker, columns: id, source_document_id, source_ticker, target_ticker, catalyst_type, pattern_confidence, signal_direction, signal_strength, relationship_strength, computed_at

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: Competitor relationship persistence round-trip

For any valid CompetitorRelationship object with valid company IDs, relationship_type, strength in [0, 1], bidirectional flag, and source, persisting it to PostgreSQL and reading it back SHALL produce an equivalent object with all fields preserved.

Validates: Requirements 1.1, 7.1

Property 2: Competitor query completeness and ordering

For any set of competitor relationships involving a company (as either company_a or company_b), querying competitors for that company SHALL return all active relationships containing that company, and the results SHALL be ordered by strength descending.

Validates: Requirements 1.2

Property 3: Soft-delete preserves row

For any active competitor relationship, deleting it SHALL set active = False while preserving the row in the database with all original field values intact.

Validates: Requirements 1.3

Property 4: Auto-inference produces valid candidates

For any company with a defined sector and industry, running auto-inference SHALL produce only candidate relationships where the candidate company shares the same sector and industry, and all produced relationships SHALL have source = 'inferred' with strength in [0, 1].

Validates: Requirements 2.1, 2.3

Property 5: Auto-inference ranks by co-mention frequency

For any set of candidate competitors with different co-mention counts in document_company_mentions, the auto-inferred relationships SHALL have strength scores that are monotonically non-decreasing with co-mention frequency — candidates with more co-mentions receive higher or equal strength scores.

Validates: Requirements 2.2

Property 6: Auto-inference idempotence

For any company, running auto-inference twice in succession SHALL produce the same set of relationships (no duplicates created), with strength scores updated to reflect the latest co-mention data.

Validates: Requirements 2.4

Property 7: Pattern computation correctness

For any set of historical document_impact_records and trend_windows for a company-catalyst pair (or cross-company pair), the computed HistoricalPattern SHALL have: sample_count equal to the actual number of matching records, bullish_pct + bearish_pct + neutral_pct ≈ 1.0, avg_strength equal to the mean of the matched trend strengths, and all fields within their valid ranges.

Validates: Requirements 3.1, 3.2, 4.2

Property 8: Pattern confidence monotonicity

For any two HistoricalPatterns where one has strictly more samples, more consistent outcomes, and more recent data than the other (all else equal), the first SHALL have a higher or equal pattern_confidence. Additionally, for any two patterns with identical statistics but different tiers, the major_corporate_decision pattern SHALL have higher confidence than the routine_signal pattern.

Validates: Requirements 3.3, 11.2

Property 9: Insufficient data threshold

For any HistoricalPattern with sample_count < 3, the pattern_confidence SHALL be below 0.3 and insufficient_data SHALL be True.

Validates: Requirements 3.4

Property 10: Valid-only data filtering

For any set of document_impact_records containing records linked to invalid intelligence (validation_status != 'valid') or rejected documents (status = 'rejected'), the Pattern_Matcher SHALL exclude those records from pattern computation — the resulting sample_count SHALL only reflect valid, non-rejected records.

Validates: Requirements 3.5

Property 11: Competitive signal strength monotonicity

For any competitive signal computation, increasing the relationship strength, pattern confidence, or source impact score (while holding others constant) SHALL produce a signal_strength that is greater than or equal to the previous value.

Validates: Requirements 4.3

Property 12: Signal propagation threshold gating

For any competitor relationship with strength < 0.2 (configurable), the Signal_Propagation_Engine SHALL produce zero competitive signals for that pair. Similarly, for any HistoricalPattern with pattern_confidence < 0.3 (configurable), the pattern SHALL be excluded from competitive signal computation.

Validates: Requirements 4.5, 9.1

Property 13: Pattern signal to WeightedSignal conversion

For any pattern-based signal converted to a WeightedSignal, the resulting object SHALL have: sentiment_value of +1.0 for bullish patterns or -1.0 for bearish patterns, impact_score equal to signal_strength * competitive_signal_weight, confidence gating applied using pattern_confidence, and recency decay based on the source document's publication time.

Validates: Requirements 5.2

Property 14: Pattern-company contradiction detection

For any set of signals where pattern-based signals have a direction opposing company-specific signals (e.g., pattern is bearish while company signals are positive), the resulting trend summary's contradiction_score SHALL be greater than zero and disagreement_details SHALL contain at least one entry.

Validates: Requirements 5.3

Property 15: Pattern evidence traceability

For any trend summary that includes pattern-based or competitive signal contributions, the top_supporting_evidence or top_opposing_evidence lists SHALL contain the source_document_id of at least one contributing pattern signal.

Validates: Requirements 5.4

Property 16: No-degradation and disabled-layer equivalence

For any company with no historical patterns or competitive signals in the aggregation window, the trend summary produced with the competitive layer enabled SHALL be identical to the summary produced with it disabled. Furthermore, for any aggregation run with the competitive layer disabled, the output SHALL be identical to company+macro-only aggregation regardless of existing pattern data.

Validates: Requirements 5.5, 6.2

Property 17: Staleness decay penalty

For any HistoricalPattern where all historical instances are older than 180 days and no instances exist within the last 90 days, the pattern_confidence SHALL be strictly less than the confidence computed for an identical pattern with at least one instance within the last 90 days.

Validates: Requirements 9.2

Property 18: Pattern-only suppression

For any trend summary where the trend direction is driven solely by pattern-based and competitive signals (no company-specific or macro signals support the direction), the resulting recommendation SHALL have mode = 'informational' and the thesis SHALL contain a pattern-only caveat.

Validates: Requirements 9.3

Property 19: Catalyst tier classification determinism

For any catalyst type, the tier classification SHALL be deterministic: m_and_a, legal, restructuring, leadership_change, strategic_pivot, buyback, and dividend_change SHALL always map to major_corporate_decision; all other catalyst types SHALL map to routine_signal.

Validates: Requirements 11.1

Property 20: Major decision extended lookback

For any pattern mining query for a major_corporate_decision catalyst type, the lookback window SHALL be 365 days. For any routine_signal catalyst type, the lookback window SHALL be 180 days. This applies to both self-company and cross-company pattern queries.

Validates: Requirements 11.3, 11.5

Property 21: Competitive signal persistence round-trip

For any valid CompetitiveSignalRecord with all required fields (source_document_id, source_ticker, target_ticker, catalyst_type, pattern_confidence, signal_direction, signal_strength, relationship_strength, computed_at), persisting it to PostgreSQL and reading it back SHALL produce an equivalent record with all fields preserved.

Validates: Requirements 4.4, 7.2

Error Handling

Pattern Mining Failures

  • Database errors during historical pattern queries are logged and the pattern is treated as "no data" — the aggregation engine continues with company-specific and macro signals only.
  • Malformed or missing trend_windows data for a historical period results in that period being excluded from pattern computation (reduced sample_count) rather than failing the entire query.

Signal Propagation Failures

  • If competitor relationship lookup fails, propagation is skipped for that ticker and logged. Aggregation continues with self-company patterns only.
  • If pattern mining fails for a specific competitor, that competitor is skipped. Other competitors are still processed.
  • Sustained propagation errors exceeding a configurable threshold (default 5 consecutive failures) trigger an operator alert via the existing alerting framework.

Auto-Inference Failures

  • If the document_company_mentions table is empty or the query fails, auto-inference returns an empty candidate list with a warning. No relationships are created or modified.
  • If sector/industry data is missing for the target company, inference is skipped with a 400 response.

Competitor Registry Failures

  • Attempting to create a relationship between the same company (company_a_id == company_b_id) returns a 400 error.
  • Attempting to create a duplicate active relationship returns a 409 conflict.
  • Foreign key violations (non-existent company IDs) return a 404 error.

Runtime Toggle Safety

  • Toggle state is read from PostgreSQL at the start of each aggregation cycle — same pattern as the macro toggle, no caching.
  • Toggle changes are audit-logged with operator identity, previous state, and new state.
  • Disabling the competitive layer does not delete any data — pattern mining remains queryable via the API, only aggregation integration is skipped.

Graceful Degradation

  • The competitive layer is designed to be fully optional. Any failure in pattern mining, signal propagation, or competitive signal computation results in the aggregation engine falling back to company-specific + macro signals with no degradation of existing behavior.

Testing Strategy

Property-Based Testing

This feature is well-suited for property-based testing. The core logic — pattern confidence computation, signal strength weighting, threshold gating, catalyst tier classification, and overlap/monotonicity properties — consists of pure functions with clear input/output behavior and a large input space.

Library: Hypothesis for Python property-based testing.

Configuration: Minimum 100 iterations per property test.

Tag format: Feature: competitive-historical-patterns, Property {number}: {property_text}

Each correctness property maps to one property-based test. Generators will produce:

  • Random CompetitorRelationship objects with valid relationship types, strength in [0, 1], and source values
  • Random HistoricalPattern objects with valid sample counts, percentage distributions summing to ~1.0, and confidence scores
  • Random CompetitiveSignalRecord objects with valid direction, strength, and confidence values
  • Random sets of WeightedSignal objects with mixed sentiment values for contradiction testing
  • Random catalyst types drawn from both major decision and routine signal categories

Unit Testing

Unit tests complement property tests for specific examples and edge cases:

  • API endpoint response codes and error handling (CRUD operations, validation errors, 404s, 409s)
  • Dashboard component rendering with mock data (competitors panel, patterns panel, signals panel, decision timeline)
  • Toggle state transitions and audit logging
  • Auto-inference with empty data, single company, no co-mentions
  • Pattern mining with zero results, exactly 3 results (boundary), mixed valid/invalid records

Integration Testing

Integration tests verify end-to-end flows:

  • Full aggregation cycle with competitive layer enabled: document intelligence → pattern mining → signal propagation → trend summary
  • Lake publisher producing Parquet datasets for competitor relationships and competitive signals
  • Toggle disable/re-enable cycle preserving data integrity
  • API endpoints returning correct data from PostgreSQL
  • Dashboard pages rendering with live API data