# System Design — standupmate: GitHub-backed Daily Standup Journal

## Executive Summary

`standupmate` is a single-user CLI tool that generates daily standup drafts from GitHub activity. It reads commits, merged pull requests, and optionally closed issues from a configurable work window, compares them with end-of-day close notes stored in a GitHub journal repository, then produces a Markdown standup report with completed work, carry-over tasks, AI-suggested next steps, and a plan-vs-actual comparison. An optional AI layer (OpenAI or Gemini) enriches the output; on failure the system falls back to deterministic rule-based generation. There is no backend server or database — GitHub serves as both the activity source and the journal storage.

## Scope and Assumptions

| Item | Decision |
|---|---|
| Product type | Single-user CLI tool, distributed as `standupmate` npm package |
| Users | 1 (the developer running the tool) |
| Activity source | GitHub REST API — commits, merged PRs, closed issues across repos owned by the configured username |
| Plan source | End-of-day close notes (`standupmate close`), persisted as JSON in the journal repo |
| History storage | GitHub journal repo — Markdown reports under `standups/YYYY/MM/DD.md`, machine-readable notes under `logs/YYYY/MM/DD.json` |
| AI | Optional, provider-agnostic (OpenAI + Gemini); fallback to rule-based on any error |
| Backend / database | None — zero-infrastructure design |
| Work window | Last N calendar days (default 7), anchored to current time in a configurable IANA timezone |
| Runtime | Node.js ≥ 20, zero npm dependencies |

## High-Level Architecture

```mermaid
flowchart TD
    Dev[Developer] --> CLI["CLI Entry (src/index.js)"]
    CLI --> Args["Argument Parser (cli/args.js)"]
    Args --> Config["Config Loader (config/configLoader.js)"]
    Config --> Token["GitHub PAT"]
    Token --> Client["GitHub API Client (github/githubClient.js)"]
    Client --> ActivitySvc["Activity Service (github/activityService.js)"]
    Client --> JournalRW["Journal Read/Write (getContent / putContent)"]
    ActivitySvc --> Raw["Raw commits, PRs, issues"]
    JournalRW --> Logs["Close notes (logs/YYYY/MM/DD.json)"]
    Raw --> Normalize["Activity Normalizer (activity/normalizer.js)"]
    Logs --> Compare["Plan vs Actual Comparator (activity/comparator.js)"]
    Normalize --> Report["Report Builder (index.js)"]
    Compare --> Report
    Report --> AIChoice{"AI configured + key present?"}
    AIChoice -- Yes --> AI["AI Writer: OpenAI or Gemini"]
    AIChoice -- No --> Rule["Rule-based assembly"]
    AI --> AIFallback{"HTTP 2xx + valid JSON?"}
    AIFallback -- No --> Rule
    AIFallback -- Yes --> Format["Markdown Formatter (formatter/markdownFormatter.js)"]
    Rule --> Format
    Format --> Preview["stdout preview"]
    Format --> Publish["PUT standups/YYYY/MM/DD.md"]
    Format --> Local["--output to local file"]
```

## Core Workflows

### Generate daily standup (`standupmate today`)

```mermaid
sequenceDiagram
    actor Dev as Developer
    participant CLI as CLI
    participant GH as GitHub API
    participant Journal as Journal Repo
    participant Cmp as Comparator
    participant AI as AI Provider

    Dev->>CLI: standupmate today [--repo owner/repo] [--days 7] [--dry-run] ...
    CLI->>CLI: Load ~/.standupmate/config.yml + env vars
    CLI->>GH: GET /users/{username}/repos (or /user/repos if authenticated)
    GH-->>CLI: Active repos (filtered: owner match, not archived, pushed within window)
    loop Per repo (200 ms delay)
        CLI->>GH: GET /repos/{repo}/commits + search PRs + search issues (parallel)
        GH-->>CLI: Raw items with __repo tag
    end
    CLI->>Journal: GET /repos/{journal}/contents/logs/YYYY/MM/DD.json
    Journal-->>CLI: Previous close notes (or 404 → first-run mode)
    CLI->>Cmp: Keyword + issue-number matching (threshold 0.45)
    Cmp-->>CLI: { completed, carryOver, unplannedCompleted, evidence }
    CLI->>AI: POST activity titles + close notes (15 s timeout)
    AI-->>CLI: { summary, today[] } JSON (or timeout/error → fallback)
    CLI->>Journal: PUT /repos/{journal}/contents/standups/YYYY/MM/DD.md (with SHA if update)
    CLI-->>Dev: Print formatted Markdown + report URL
```

### Log end-of-day notes (`standupmate close`)

```mermaid
sequenceDiagram
    actor Dev as Developer
    participant CLI as CLI
    participant GH as GitHub API
    participant Journal as Journal Repo

    Dev->>CLI: standupmate close "Implemented token validation"
    CLI->>CLI: Load config
    alt --repo not provided
        CLI->>GH: GET user repos (filtered, excluding journal repo)
        GH-->>CLI: Recent repos
        Dev->>CLI: Interactive pick (1..N or Enter to skip)
    end
    CLI->>Journal: GET logs/YYYY/MM/DD.json
    Journal-->>CLI: Existing entries or 404
    CLI->>Journal: PUT logs/YYYY/MM/DD.json (upsert: attach SHA on update)
    CLI-->>Dev: Printed URL to logs file on GitHub
```

## CLI Interface

```
standupmate today                    Show & publish today's report
standupmate close "note"             Log end-of-day note
standupmate --help                   Show this help

Options:
  --repo owner/repo         Override source repository
  --days N                  Look-back window in days (default: 7)
  --include-issues          Include closed issues in activity
  --no-pull-requests        Exclude merged pull requests
  --dry-run                 Print report without publishing
  --debug                   Enable [debug] log lines
  --output PATH             Write Markdown to local file
```

## Configuration

Config file: `~/.standupmate/config.yml`. Environment variables override file values.

```yaml
github:
  username: your-github-username
  # token: ""            # ← or GITHUB_TOKEN

journal:
  repo: your-username/daily-journal

standup:
  timezone: Asia/Ho_Chi_Minh
  days: 7

ai:
  provider: gemini        # "gemini" | "openai"
  model: gemini-2.5-flash
  # apiKey: ""            # ← or OPENAI_API_KEY / GEMINI_API_KEY
  # baseUrl: ""           # optional custom endpoint
```

**Resolution precedence:** CLI flag → env var → config file → hard-coded default.

| Env Variable | Overrides |
|---|---|
| `GITHUB_TOKEN` | `github.token` |
| `OPENAI_API_KEY` / `GEMINI_API_KEY` | `ai.apiKey` |
| `GITHUB_USERNAME` | `github.username` |
| `JOURNAL_REPO` | `journal.repo` |
| `TIMEZONE` | `standup.timezone` |
| `DAYS` | `standup.days` |

## Key Design Decisions

| Decision | Choice | Rationale | Trade-off |
|---|---|---|---|
| Interface | CLI | Developer-native workflow; zero UI overhead | No visual dashboard |
| Activity source | GitHub REST API | Richer than local Git: PRs, issues, metadata | Requires PAT + internet |
| Storage | GitHub journal repo | Versioned, browsable via github.com, zero infrastructure | Write-permission needed on journal repo |
| Journal layout | `standups/YYYY/MM/DD.md` + `logs/YYYY/MM/DD.json` | Human-readable + machine-readable; hierarchical by date | Two files per day |
| Plan source | Explicit close notes | Developer intent is unambiguous; no heuristics needed | Requires running `close` at end of day |
| AI integration | Direct HTTP fetch (no SDK) | Zero npm dependencies; provider-agnostic | Manual prompt engineering |
| Comparison algorithm | Keyword overlap (Jaccard-like) + issue-number exact match | Simple, deterministic, no training data | May miss semantic matches |
| Auth | GitHub fine-grained PAT + AI API key | Standard, revocable, scoped | Manual setup per machine |
| Config | YAML file + env vars | Human-editable, overridable in CI | YAML in home directory |
| Dependencies | Zero runtime deps | Minimal attack surface, instant install | No framework conveniences |

## Data Model

### ActivityItem (normalized from raw API responses)

```typescript
interface ActivityItem {
  id: string;              // commit SHA or PR/issue GitHub ID
  type: "commit" | "pull_request" | "issue";
  title: string;           // first line of commit message, or PR/issue title
  actor: string;           // GitHub login
  occurredAt: string;      // ISO-8601 timestamp
  url: string;             // html_url from API
  metadata: {
    repo: string;          // owner/repo
    sha?: string;          // short 7-char SHA (commits only)
    number?: number;       // PR/issue number
  };
}
```

**Noise filtering:** Items whose title matches `/^(wip|update|misc|merge branch|merge pull request)$/i` are discarded during normalization.

### StandupReport (in-memory, passed to formatter)

```typescript
interface StandupReport {
  date: string;                          // YYYY-MM-DD in configured timezone
  repos: { repo: string; items: ActivityItem[] }[];      // full N-day window, sorted by activity count desc
  yesterdayByRepo: { repo: string; items: ActivityItem[] }[]; // last 24 h only
  logs: { time: string; repo: string | null; note: string }[]; // close-note entries
  comparison?: ComparisonResult;         // present only if close notes exist
  aiSummary?: string;                    // 1–2 sentence AI summary
  aiToday?: string[];                    // AI-suggested tasks for today
}
```

### ComparisonResult

```typescript
interface ComparisonResult {
  completed: string[];          // planned items matched to activity
  carryOver: string[];          // planned items with no matching activity
  unplannedCompleted: string[]; // activity titles not matched to any plan
  evidence: {                   // match provenance for auditing
    plannedItem: string;
    matchedActivityIds: string[];
    confidence: number;         // 0.45–1.00
  }[];
}
```

**Matching algorithm:** Each planned item is compared against every activity title.
- Exact issue-number match (`#123` ↔ `#123`) → confidence = 1.00.
- Otherwise, Jaccard-like keyword overlap on deduplicated stems (stop words removed) → confidence ∈ [0, 1].
- Items with confidence ≥ 0.45 are considered matched; the highest-confidence match wins.

### Close Notes persisted to `logs/YYYY/MM/DD.json`

```json
{
  "date": "2026-06-27",
  "entries": [
    {
      "time": "2026-06-27T18:45:00.000Z",
      "repo": "owner/backend",
      "note": "Implemented token validation"
    }
  ]
}
```

## Generated Markdown Format

```markdown
# Daily Standup - 2026-06-28

## Summary
AI-generated 1–2 sentence recap (only when AI enabled and successful)

## Today
- {{close-note entries with optional `repo` prefix}}
- {{AI-suggested tasks}}

## Yesterday
### `owner/repo`
- {{up to 12 items}}
_N commits_

## This Week
### `owner/repo`
- {{up to 12 items}}
_N commits_

## Plan vs Actual
### Completed from Previous Plan
- {{matched planned items}}
### Carried Over
- {{unmatched planned items}}
### Unplanned Completed
- {{activity not matching any plan}}

## Evidence
### `owner/repo` Commits
- `abc1234` fix: resolve login bug (https://github.com/owner/repo/commit/abc1234)
```

**Display rules:** Repo sections cap at 12 items; Evidence shows all commits with short SHA and full URL. Empty sections render "- No activity" or "None".

## GitHub API Client Details

The `GitHubClient` wraps `fetch()` with no SDK dependency.

| Method | Endpoint | Notes |
|---|---|---|
| `requestAll(path)` | Paginated GET | Follows `Link` header; max 10 pages; warns if truncated |
| `request(path, {method, body})` | Single request | Returns parsed JSON |
| `fetchAccountRepositories()` | `/users/{u}/repos` or `/user/repos` | Auth-dependent; filters by owner, push window, non-archived |
| `fetchCommits()` | `/repos/{r}/commits` | `since`, `until`, `author` params; 100 per page |
| `fetchMergedPullRequests()` | `/search/issues` | Query: `repo:{r} type:pr is:merged merged:>={date} author:{u}` |
| `fetchClosedIssues()` | `/search/issues` | Query: `repo:{r} type:issue is:closed closed:>={date} assignee:{u}` |
| `getContent()` | `/repos/{r}/contents/{path}` | Returns `{content, sha}` or null on 404 |
| `putContent()` | PUT same endpoint | Base64-encodes content; attaches SHA for updates |

**Rate limiting:** Non-2xx responses extract `x-ratelimit-reset` header and expose it on `GitHubError.rateLimitReset`. Unauthenticated mode warns when tracking > 5 repos (60 req/h limit). Authenticated requests have 5,000 req/h.

## AI Integration

AI is called directly via `fetch()` to provider REST endpoints — no SDK dependency.

| Provider | Endpoint | Auth | Response path |
|---|---|---|---|
| OpenAI | `{baseUrl}/chat/completions` | `Authorization: Bearer {key}` | `data.choices[0].message.content` |
| Gemini | `{baseUrl}/models/{model}:generateContent?key={key}` | Query parameter | `data.candidates[0].content.parts[0].text` |

**Prompt:** A single system-style user message containing yesterday's GitHub activity (type + title per repo) and close notes. The model returns strict JSON `{ "today": string[], "summary": string }`. Temperature is fixed at 0.3.

**Resilience:** 15-second `AbortController` timeout. Any failure (HTTP error, timeout, invalid JSON) silently falls back to rule-based assembly. The fallback report omits the Summary section and AI-suggested Today items.

## Failure Handling

| Scenario | Behavior |
|---|---|
| Missing `GITHUB_TOKEN` | `ConfigError`, exit 1 with setup instructions |
| Previous close notes missing (404) | First-run mode — skip comparison, no Plan vs Actual section |
| GitHub rate limit (429 / 403) | `GitHubError` with `rateLimitReset` ISO timestamp; CLI exits |
| Unauthenticated + > 5 repos | Warning only; continues (risk of 403 at runtime) |
| Per-repo API error | Warning, skip that repo, continue with remaining repos |
| AI timeout / HTTP error / bad JSON | Log warning, fall back to rule-based report |
| Publish conflict (SHA mismatch) | Already handled: `getContent` fetches latest SHA before every `putContent` |
| Publish failure (network / permissions) | Error logged, formatted Markdown still printed to stdout |
| `--days` < 1 or non-integer | `ConfigError` before any API call |

## Security

- Secrets (`GITHUB_TOKEN`, AI API key) are read from `~/.standupmate/config.yml` or environment variables; **never** printed, logged, or published.
- AI receives only normalized activity titles and close-note text — no source code, diffs, file paths, or repository contents.
- GitHub token scope: recommend fine-grained PAT with `contents: read/write` on the journal repo + `metadata: read` on source repos.
- The journal repo should be private if standup reports contain sensitive work information.
- Error messages sanitize unexpected exceptions to avoid leaking stack traces (full trace only with `DEBUG` env).

## Component Map

| Source Module | Responsibility |
|---|---|
| `src/index.js` | Entry point, orchestration, AI fetch, publish logic |
| `src/cli/args.js` | CLI argument parsing, help text |
| `src/config/configLoader.js` | YAML + env-var config loading with validation |
| `src/github/githubClient.js` | GitHub REST API wrapper (fetch, pagination, rate-limit) |
| `src/github/activityService.js` | Multi-repo activity aggregation, repo resolution |
| `src/activity/normalizer.js` | Raw → `ActivityItem` normalization, noise filtering |
| `src/activity/comparator.js` | Plan-vs-actual keyword + issue-number matching |
| `src/formatter/markdownFormatter.js` | `StandupReport` → Markdown string |
| `src/output/outputWriter.js` | `--output` local file writer with mkdir -p |
| `src/shared/dateWindow.js` | Timezone-aware date keys, work-window calculation |
| `src/shared/errors.js` | Typed error hierarchy (`AppError`, `ConfigError`, `GitHubError`) |
| `src/shared/logger.js` | Leveled logger (info, warn, error, debug) |
| `src/shared/text.js` | Keyword extraction, stop-word filtering, issue-number parsing |

## Test Coverage

12 test files under `test/` using Node.js native test runner (`node --test`):

| Test file | Covers |
|---|---|
| `args.test.js` | CLI parsing, validation, defaults |
| `configLoader.test.js` | YAML parsing, env-var merging, validation |
| `githubClient.test.js` | API wrapper, pagination, errors, content CRUD |
| `activityService.test.js` | Repo resolution, aggregation, per-repo error isolation |
| `normalizer.test.js` | Activity normalization, noise filtering |
| `comparator.test.js` | Keyword matching, issue-number matching, edge cases |
| `markdownFormatter.test.js` | Full report rendering, sections, slicing |
| `dateWindow.test.js` | Timezone date keys, window math |
| `errors.test.js` | Error hierarchy |
| `logger.test.js` | Log level filtering |
| `outputWriter.test.js` | Local file output |
| `text.test.js` | Keyword extraction, stop words |
