Pipeline Nowcast Spec
How do you know the factory is on track before the weekly meeting tells you it isn't?
Build Contract
| # | Feature | Function | Outcome | Job | State |
|---|---|---|---|---|---|
| 1 | Signal Collectors (5x) | Extract raw measurements from each source table | All 5 sources feeding one algorithm | Collect | Gap |
| 2 | Signal Normalization | Normalize each signal to 0-1 scale per type | Apples-to-apples comparison | Normalize | Gap |
| 3 | Exponential Decay | Weight signals by recency with configurable half-life | Recent signals matter more | Weight | Gap |
| 4 | Variance Computation | Per-signal (actual - forecast) / forecast | Know which signal is drifting | Measure | Gap |
| 5 | Composite Scoring | Weighted sum with configurable signal weights | Single number: are we on track? | Compose | Gap |
| 6 | Classification | Map composite to on_track / warning / critical | Status matches varianceStatus enum | Classify | Gap |
| 7 | Confidence Calculation | Score based on signal coverage and freshness | Trust the number or gather more data | Calibrate | Gap |
| 8 | Recommendations Engine | Top risk, top momentum, action list | Know what to do next, not just status | Advise | Gap |
| 9 | Constants Registration | PIPELINE_NOWCAST block in algorithm-constants.ts | All thresholds tunable without code | Configure | Gap |
| 10 | Forecast Baselines | Target values per signal type (config) | Variance has something to compare to | Baseline | Gap |
Principles
What truths constrain the design?
The Job
| Element | Detail |
|---|---|
| Situation | Five signal systems built independently. Each has a dashboard. None connect. |
| Intention | A single composite score answering "are we on track, drifting, or in trouble?" |
| Obstacle | No algorithm normalizes, weights, and composes these signals with recency decay. |
Why Now
All five signal sources exist in production:
- CRM: 5 deals, 10 activities, $1.2M pipeline (commissioning 2026-03-02)
- Agent comms: Convex event stream with typed messages
- Commissioning: 20 features at L0-L3
- Predictions: schemas deployed, tables empty
The data exists. The synthesis doesn't. Every commissioning session (45 min manual) could be a 2-second algorithm call.
Design Constraints
| Constraint | Rationale |
|---|---|
| Pure function, no side effects | Testable, composable, matches algorithm framework |
| All thresholds in constants file | Tunable without code changes |
| Minimum 3 signals for confidence > 0.5 | Prevents false confidence from sparse data |
Match existing varianceStatus enum | Interop with prediction schemas |
| Exponential decay, not linear | Recent signals should dominate, old signals fade smoothly |
Refusal Spec
| Category | Action | Response |
|---|---|---|
| Insufficient signals | Fewer than 1 signal available | Return confidence: 0, status: critical, reasoning: "No signals" |
| Stale data | All signals older than 2x half-life | Return confidence below 0.3, flag staleness |
| Invalid forecasts | Forecast value is 0 or negative | Skip signal, reduce confidence, log warning |
Performance
How do we know it's working?
Priority Score
| Dimension | Score | Evidence |
|---|---|---|
| Pain | 4 | 45 min/session manual synthesis across 5 dashboards. No composite view. Drift invisible between meetings. |
| Demand | 3 | Internal demand (dream team commissioning). No external customer yet. |
| Edge | 4 | Proprietary signal combination (CRM + agent comms + commissioning state). Nobody else has this data shape. |
| Trend | 4 | Nowcasting is the pattern in AI operations. Real-time variance > batch forecasting. |
| Conversion | 2 | Internal tool first. Path to customer-facing via BOaaS later. |
| Composite | 384 | 4 x 3 x 4 x 4 x 2. Strong candidate. Edge and trend validated, conversion needs customers. |
Quality Targets
| Metric | Target | Method |
|---|---|---|
| Execution time | <500ms for 5 signals | Benchmark test |
| Classification accuracy | Matches manual assessment 5 consecutive days | Human comparison |
| Signal coverage confidence | Degrades gracefully below 3 signals | Unit test with partial inputs |
Eval Strategy
| What | How | When |
|---|---|---|
| Classification accuracy | Compare nowcast status vs dream team manual assessment | Daily for first 2 weeks |
| Signal freshness | Check timestamp of newest signal per type | Every run |
| Threshold calibration | Review false positive/negative rate | Weekly for first month |
Kill signal: Nowcast status disagrees with manual assessment for 5 consecutive days after 2-week calibration period. Algorithm is wrong or signals are wrong.
Platform
What do we control?
Current State
| Component | Built | Wired | Working | Notes |
|---|---|---|---|---|
| Algorithm framework | Yes | Yes | Yes | libs/agency/src/lib/algorithms/ |
AlgorithmMetadata pattern | Yes | Yes | Yes | Standard export |
algorithm-constants.ts | Yes | Yes | Yes | Extensible |
| CRM data (Drizzle) | Yes | Yes | Yes | 5 deals, 10 activities |
| Convex agent messages | Yes | Yes | Yes | HTTP client working |
| Commissioning state | Yes | Partial | Partial | Manual markdown, needs parser |
| Prediction schemas | Yes | No | No | Tables empty |
| Signal normalization | No | No | No | Build |
| Exponential decay | No | No | No | Build |
| Composite scoring | No | No | No | Build |
| Insights UI | Yes | No | No | Components exist, no data |
Build Ratio
~70% composition, ~30% new code.
Algorithm Interface
Input
interface NowcastInput {
pipeline: {
deals: Array<{ amount: number; probability: number; stage: string; closeDate: string }>;
targetCoverage: number; // default 3.0x
};
activity: {
activities: Array<{ type: string; outcome: string; startDate: string }>;
targetPerDealPerWeek: number; // default 2
dealCount: number;
};
agentVelocity: {
messages: Array<{ type: string; createdAt: number }>;
plansCompleted: number;
blockersOpen: number;
};
commissioning: {
features: Array<{ name: string; currentLevel: number; forecastLevel: number }>;
};
predictions: {
entries: Array<{ confidenceScore: number; accuracyScore: number | null; status: string }>;
};
config?: {
weights?: Partial<SignalWeights>;
decayHalfLifeDays?: number;
thresholds?: { onTrack?: number; warning?: number };
};
}
Output
interface NowcastResult {
result: {
composite: number; // 0-1
status: "on_track" | "warning" | "critical";
signals: NowcastSignal[]; // per-signal breakdown
topRisk: string; // highest-variance signal name
topMomentum: string; // most-improving signal name
};
metadata: {
algorithm: "pipeline-nowcast";
version: string;
executionTimeMs: number;
signalCount: number;
};
reasoning: string[];
confidence: number; // 0-1 based on signal coverage
recommendations: {
status: "on_track" | "warning" | "critical";
actions: string[];
signalsNeedingAttention: string[];
};
}
interface NowcastSignal {
name: string;
score: number; // 0-1 normalized
weight: number; // configured weight
variance: number; // (actual - forecast) / forecast
trend: "improving" | "stable" | "declining";
freshness: number; // 0-1 decay factor
}
Constants
export const PIPELINE_NOWCAST = {
PIPELINE_WEIGHT: 0.35,
ACTIVITY_WEIGHT: 0.25,
AGENT_VELOCITY_WEIGHT: 0.15,
COMMISSIONING_WEIGHT: 0.15,
PREDICTION_WEIGHT: 0.1,
DECAY_HALF_LIFE_DAYS: 14,
ON_TRACK_THRESHOLD: 0.7,
WARNING_THRESHOLD: 0.4,
MIN_SIGNALS_FOR_CONFIDENCE: 3,
TARGET_PIPELINE_COVERAGE: 3.0,
TARGET_ACTIVITY_PER_DEAL_WEEK: 2,
} as const;
Core Algorithm
1. For each signal type with data:
a. Normalize raw measurement to 0-1 scale
b. Apply exponential decay: factor = exp(-0.693 * ageDays / halfLife)
c. Compute variance: (actual - forecast) / forecast
d. Record trend from last 3 measurements
2. Compute confidence:
confidence = signalsPresent / totalSignals * avgFreshness
3. Compute composite:
composite = sum(signal.score * signal.weight * signal.freshness)
/ sum(signal.weight * signal.freshness)
4. Classify:
>= 0.7 → on_track
>= 0.4 → warning
< 0.4 → critical
5. Generate recommendations:
topRisk = signal with lowest score
topMomentum = signal with best trend
actions = per-signal actionable suggestions
Protocols
How does the system coordinate?
Build Order
| Sprint | Features | What | Effort | Acceptance |
|---|---|---|---|---|
| S0 | #9, #10 | Constants + forecast baselines | 0.5d | Constants in file, baselines documented |
| S1 | #1, #2, #3 | Signal collectors + normalization + decay | 2d | Each collector returns normalized 0-1 with decay |
| S2 | #4, #5, #6, #7 | Variance + composite + classification + confidence | 2d | calculateNowcast() returns valid NowcastResult with 5 mock signals |
| S3 | #8 | Recommendations engine | 1d | topRisk, topMomentum, actions populated from signal breakdown |
| S4 | — | Wire to production data sources | 1d | Real signals flowing, composite rendered in Insights |
Commissioning
| # | Feature | Install | Test | Operational | Optimize |
|---|---|---|---|---|---|
| 1 | Signal Collectors (5x) | --- | --- | --- | --- |
| 2 | Signal Normalization | --- | --- | --- | --- |
| 3 | Exponential Decay | --- | --- | --- | --- |
| 4 | Variance Computation | --- | --- | --- | --- |
| 5 | Composite Scoring | --- | --- | --- | --- |
| 6 | Classification | --- | --- | --- | --- |
| 7 | Confidence Calculation | --- | --- | --- | --- |
| 8 | Recommendations Engine | --- | --- | --- | --- |
| 9 | Constants Registration | --- | --- | --- | --- |
| 10 | Forecast Baselines | --- | --- | --- | --- |
Agent-Facing Spec
Commands: pnpm test -- --filter=pipeline-nowcast, pnpm tc
Boundaries:
- Always: pure function, no DB writes, no side effects
- Ask first: threshold changes, weight adjustments
- Never: modify signal source data, bypass confidence check
Test Contract:
| # | Feature | Test File | Assertion |
|---|---|---|---|
| 1 | Signal collectors | pipeline-nowcast.test.ts | Each collector returns 0-1 from valid input |
| 2 | Normalization | pipeline-nowcast.test.ts | Edge cases: zero, negative, very large values |
| 3 | Exponential decay | pipeline-nowcast.test.ts | 14-day-old signal at 50% weight |
| 4 | Composite scoring | pipeline-nowcast.test.ts | 5 signals produce weighted sum |
| 5 | Classification | pipeline-nowcast.test.ts | Boundary: 0.7 on_track, 0.4 warning |
| 6 | Confidence | pipeline-nowcast.test.ts | 2 of 5 signals = confidence < 0.5 |
| 7 | Recommendations | pipeline-nowcast.test.ts | topRisk = lowest-scoring signal |
| 8 | Cold start | pipeline-nowcast.test.ts | 0 signals = confidence 0, status critical |
Players
Who creates harmony?
Job 1: Know If We're On Track
| Element | Detail |
|---|---|
| Struggling moment | Weekly commissioning session: 45 min checking 5 dashboards, forming a mental picture |
| Workaround | Manual synthesis, gut feel, "seems fine" until it isn't |
| Progress | Glance at one composite score, see which signal is drifting, act on the recommendation |
| Hidden objection | "A single number can't capture this complexity" |
| Switch trigger | Missed a regression that was visible in the data 3 days earlier |
Features that serve this job: #5, #6, #8
Job 2: Detect Drift Early
| Element | Detail |
|---|---|
| Struggling moment | Problem compounds silently between reviews. Activity drops, nobody notices for a week. |
| Workaround | Hope someone checks. Rely on agent posting to #meta. |
| Progress | Nowcast fires warning when activity velocity drops below threshold, before the weekly review |
| Hidden objection | "False alarms are worse than no alarms" |
| Switch trigger | A deal went cold because nobody noticed zero activity for 10 days |
Features that serve this job: #3, #4, #7
Relationship to Other PRDs
| PRD | Relationship | Data Flow |
|---|---|---|
| Sales CRM & RFP | Peer | Pipeline + activity signals flow IN to nowcast |
| Agent Platform | Peer | Agent velocity signals flow IN from Convex |
| ETL Data Tool | Peer (upstream) | Depends on data flowing correctly |
| Sales Process Optimisation | Peer (downstream) | Nowcast output could feed SPO decision engine |
Context
- Pictures — Pre-flight maps that feed this spec
- Prompt Deck — Sales compression of this spec
- Commissioning Dashboard — The scoreboard this algorithm reads
- AI Product Requirements — Section definitions