diff --git a/CHANGELOG.md b/CHANGELOG.md index 48732d8..58bc1d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## Unreleased + +### Growth Studio + +- Added a dedicated `/growth` shell with repository workspaces for Missions, interventions and opportunity scans, editorial plans, month and week calendars, Queue, Library, and weekly Review. +- Added an additive, idempotent account-scoped SQLite model with one-shot migration of legacy goal suggestions and proposals into first-class interventions and content drafts. +- Added a media-complete editorial workflow with private uploads, SSRF-guarded source imports, generated SVG cards, browser-only PNG rasterization, clipboard copy, and download fallback. +- Added manual publication recording, 48-hour and 7-day snapshot attribution, performance summaries, deterministic weekly recommendations, evergreen recycling, and bounded plan re-weighting. +- Added a colour-coded unified calendar, account-wide filters and ICS export, Growth defaults, and atomic multi-repository planning with pillar-date deconfliction. +- Publishing remains copy-paste only: Gitdeck stores no social credentials and sends no outbound social posts. + ## [1.0.8](https://github.com/debba/gitdeck/compare/v1.0.7...v1.0.8) (2026-09-03) diff --git a/Dockerfile b/Dockerfile index 4508e58..162abc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG NODE_BUILDER_VERSION=22-slim -ARG NODE_RUNTIME_VERSION=22-alpine +ARG NODE_RUNTIME_VERSION=22-slim FROM node:${NODE_BUILDER_VERSION} AS builder WORKDIR /app @@ -12,7 +12,8 @@ COPY CHANGELOG.md ./ COPY src ./src COPY public ./public -RUN npm run build +RUN npm run build \ + && npm prune --omit=dev FROM node:${NODE_RUNTIME_VERSION} AS runtime @@ -23,6 +24,7 @@ ENV NODE_ENV=production \ PORT=8765 COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/dist ./dist COPY --from=builder /app/index.html ./index.html COPY docker-entrypoint.sh ./docker-entrypoint.sh diff --git a/README.md b/README.md index 08de6d7..c319ca5 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,69 @@ The dashboard pulls data from the GitHub REST and GraphQL APIs and organizes it - **Issues / Pull Requests** — cross-repo lists with the same filter sidebar, useful for triage across many projects. - **Insights** — overview of all repos with alerts ("issues need attention", "security alerts need attention", "no push for X days"), opportunities, and correlations between traffic and recent activity. Each repo gets a status (Strong / Watch / Risky). - **Alerts** — dedicated security-alert view for Dependabot and code scanning findings, so you can jump straight to the repos that need attention. -- **Daily digest** — short per-repo summary of the day's movement (stars, forks, issues), with an executive summary you can copy as Markdown. Optionally augmented by an OpenAI-generated narrative when `OPENAI_API_KEY` is configured. +- **Daily digest** — short per-repo summary of the day's movement (stars, forks, issues), with an executive summary you can copy as Markdown. Optionally augmented by an AI-generated narrative when an [AI provider](#ai-integration) is configured. - **Board** — Kanban-style view that groups issues into columns (Backlog, To-do, In progress, Ready, In review, etc.). +- **Growth Studio** — a dedicated, repository-centric workspace for missions, opportunity scans, editorial plans, mandatory media, manual publishing, attribution, weekly reviews, and coordinated multi-repository calendars. + +## Growth Studio + +Growth Studio turns repository signals into a complete, measurable editorial workflow. Open **Growth Studio** from the main dashboard navigation: `/growth` launches in a separate window with its own top bar, repository switcher, and navigation, without the dashboard filters, tab strip, or footer. + +### Repository workflow + +Start on **Home**, where saved growth profiles and active missions are summarized across the account. Each repository then has one focused workspace: + +1. **Missions** track targets for stars, forks, closed pull requests, and release downloads. +2. **Interventions** collect AI, manual, and deterministic rule-based opportunities. An explicit opportunity scan can detect unpromoted releases or large merged pull requests, approaching star milestones, aging good-first issues, goals falling behind pace, and evergreen content ready to recycle. +3. **Library** holds the repository voice, audience, channels, cadence, content pillars, posting windows, sources, uploaded media, imported README or website media, and generated release, milestone, stats, quote, and what's-new cards. +4. **Calendar** builds one-to-four-week editorial plans from cadence and weighted pillars. Review and reschedule work in month or week view, or use **Queue** to draft, copy, and finish this week's items. +5. **Review** attributes published work to retained star and fork snapshots at 48 hours and 7 days. The weekly Growth Review reports measured outcomes, missed and upcoming work, and three next actions; later plans can apply bounded performance-based pillar re-weighting without changing the saved profile. + +Content progresses from idea to draft, ready, scheduled, and published. At least one repository-owned media attachment is required before an item can become ready, scheduled, or published. Images—including generated SVG cards—are rasterized to PNG at 2x in the browser, copied with the Clipboard API when supported, and downloaded as a fallback. + +Publishing is always manual: copy the text and image, publish them yourself, then record the publication time and optional URL in Gitdeck. Growth Studio stores no social credentials and never posts to a social network. + +The **Unified calendar** combines repositories while retaining each profile's colour, local date, and timezone. It supports month and week views, repository, channel, pillar, and status filters, account-wide ICS export, and coordinated planning for two through ten repositories. Coordinated plans move matching pillar dates when space is available so the portfolio does not compete with itself. **Settings** supplies account-scoped default timezone, cadence, and pillars for repositories that do not yet have a saved profile, and links to the existing optional AI configuration. + +### Screenshots + +These screenshots were captured from deterministic, anonymized fixture repositories. They contain no real account, token, email address, private repository, filesystem path, or user-supplied content. + +#### Home portfolio + +[![Growth Studio Home in dark theme on a desktop fixture portfolio](docs/images/growth-studio-home-dark-desktop.webp)](docs/images/growth-studio-home-dark-desktop.webp) + +[![Growth Studio Home in light theme at a mobile-representative width](docs/images/growth-studio-home-light-mobile.webp)](docs/images/growth-studio-home-light-mobile.webp) + +#### Repository Queue + +[![Growth Studio repository Queue in light theme on a desktop fixture workspace](docs/images/growth-studio-queue-light-desktop.webp)](docs/images/growth-studio-queue-light-desktop.webp) + +[![Growth Studio repository Queue in dark theme at a mobile-representative width](docs/images/growth-studio-queue-dark-mobile.webp)](docs/images/growth-studio-queue-dark-mobile.webp) + +#### Account-wide planning and Review + +[![Growth Studio unified calendar in dark theme with anonymized repository plans](docs/images/growth-studio-unified-calendar-dark-desktop.webp)](docs/images/growth-studio-unified-calendar-dark-desktop.webp) + +[![Growth Studio global weekly Review in light theme at a mobile-representative width](docs/images/growth-studio-review-light-mobile.webp)](docs/images/growth-studio-review-light-mobile.webp) + +### Upgrade, data, and operations + +Growth Studio initializes its SQLite schema automatically on first store access. Initialization is additive and idempotent, so restarting the same build or upgrading an existing installation preserves existing `repository_goals` and `repository_content_sources` rows. + +For each account, the first Growth Studio access runs a one-shot transactional migration of legacy goal suggestions: suggestions become interventions and their saved proposals become linked content drafts. The legacy JSON remains in place for compatibility but is not written again; an account-scoped migration marker prevents duplicate rows on later starts. + +Persistent data stays under `~/.gitdeck/`: + +- `~/.gitdeck/gitdeck.sqlite` stores missions, growth profiles, interventions, plans, content, asset metadata, attribution, and preferences. +- `~/.gitdeck/growth-assets/` stores private uploaded asset bytes; generated cards remain validated metadata and are rendered on request. +- Tokens, repository snapshots, and digest files remain in the same private data directory. + +Before an upgrade or storage migration, stop Gitdeck and back up the complete `~/.gitdeck/` directory so the database and asset files remain consistent. Docker users should back up the volume mounted at `/home/node/.gitdeck`. + +Asset metadata and bytes are served only through authenticated, account-scoped endpoints; stored filesystem paths are never returned to the browser. Uploaded assets are bounded and validated, while remote README or website media is revalidated through the server's SSRF guards whenever it is proxied. Forge API calls and raw snapshot reads also remain server-side, so access tokens and snapshot files are not exposed to browser code. + +AI is optional. When no provider is configured, deterministic planning, drafting, opportunity, and Review fallbacks keep the core workflow usable; provider failures are surfaced without partially persisting a coordinated plan. No native image package is required: generated cards are safe server-rendered SVG, and all rasterization and clipboard preparation happens in the browser. ### Per-repository view @@ -72,7 +133,7 @@ UI translations live in `src/i18n/`, with one dictionary file per language. See - **Node.js 20+** (anything that supports native `fetch` and ESM is fine). - A **GitHub OAuth App** with **Device Flow enabled** (see next section). -- (Optional) An **OpenAI API key** if you want AI-generated daily digest summaries. +- (Optional) An API key for **OpenAI, Anthropic, Google Gemini, OpenRouter or any OpenAI-compatible endpoint** if you want AI-generated digest narratives and Growth Studio plans, drafts, or Review narratives (see [AI integration](#ai-integration)). ## Configure GitHub @@ -127,14 +188,41 @@ The server reads its configuration from environment variables: | `GITHUB_TOKEN` | only `token` | — | Personal access token used when `GH_AUTH_MODE=token` | | `HOST` | no | `127.0.0.1` | Interface the server binds to | | `PORT` | no | `8765` | Port the server listens on | -| `OPENAI_API_KEY` | no | — | Enables AI-generated daily digest narratives | +| `AI_PROVIDER` | no | auto-detected | AI provider: `openai`, `anthropic`, `gemini`, `openrouter` or `custom`. When unset, the first provider with a key in the environment is used | +| `OPENAI_API_KEY` | no | — | OpenAI key (also enables the provider when `AI_PROVIDER` is unset) | +| `ANTHROPIC_API_KEY` | no | — | Anthropic key | +| `GEMINI_API_KEY` | no | — | Google Gemini key (`GOOGLE_API_KEY` is accepted too) | +| `OPENROUTER_API_KEY` | no | — | OpenRouter key | +| `AI_API_KEY` | no | — | Generic key for the provider selected with `AI_PROVIDER` (required for `custom` endpoints that need one) | +| `AI_MODEL` | no | per provider | Model for the provider selected with `AI_PROVIDER`. Per-provider aliases: `OPENAI_MODEL`, `ANTHROPIC_MODEL`, `GEMINI_MODEL`, `OPENROUTER_MODEL` | +| `AI_BASE_URL` | no | per provider | Endpoint override for the provider selected with `AI_PROVIDER`, e.g. `http://localhost:11434/v1` for Ollama with `AI_PROVIDER=custom` | | `GITLAB_CLIENT_ID` | no | — | Enables GitLab OAuth when paired with `GITLAB_CLIENT_SECRET` | | `GITLAB_CLIENT_SECRET` | no | — | OAuth application secret for the selected GitLab instance | | `GITLAB_REDIRECT_URI` | no | inferred from request | Exact GitLab OAuth callback URL, ending in `/api/auth/gitlab/callback` | | `GITLAB_OAUTH_INSTANCE_URL` | no | `https://gitlab.com` | GitLab instance on which the configured OAuth app is registered | -| `OPENAI_DIGEST_MODEL` | no | `gpt-4.1-mini` | Model used for digest narratives | +| `OPENAI_DIGEST_MODEL` | no | — | Legacy alias of `OPENAI_MODEL`, still honoured | | `GITDECK_DIAGNOSTICS` | no | — | Set to `1` to log provider call durations | +### AI integration + +Digest narratives and Growth Studio planning, drafting, interventions, and optional Review narratives use a pluggable AI provider. Supported providers and their default models: + +| Provider | `AI_PROVIDER` | Key variable | Default model | Default endpoint | +| ------------ | ------------- | --------------------- | --------------------- | ---------------- | +| OpenAI | `openai` | `OPENAI_API_KEY` | `gpt-4.1-mini` | `https://api.openai.com/v1` | +| Anthropic | `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-5` | `https://api.anthropic.com` | +| Google Gemini| `gemini` | `GEMINI_API_KEY` | `gemini-2.5-flash` | `https://generativelanguage.googleapis.com/v1beta` | +| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | `openai/gpt-4.1-mini` | `https://openrouter.ai/api/v1` | +| OpenAI-compatible (Ollama, Mistral, Groq, LM Studio…) | `custom` | `AI_API_KEY` (optional) | — (set `AI_MODEL`) | `http://localhost:11434/v1` | + +The configuration is layered: + +1. **Built-in defaults** (model and endpoint per provider). +2. **Environment variables** listed above. +3. **Values saved from the UI** in `~/.gitdeck/gitdeck.sqlite`, which override the environment. + +Open **Preferences → All preferences** (or go to `/preferences`) to pick the provider, store an API key, model or base URL, test the connection, and see for every field whether the value in effect comes from the database, the environment or a default. *Reset to environment* removes every stored override. Keys saved from the UI never leave the server: the API only returns a masked version. + ### Authentication modes The dashboard can obtain a GitHub token in three different ways. Pick the one that fits your setup: @@ -145,7 +233,13 @@ The dashboard can obtain a GitHub token in three different ways. Pick the one th In `gh-cli` and `token` modes the device-flow sign-in screen is hidden; the server treats the configured source as authoritative. -Tokens and snapshots are persisted under `~/.gitdeck/`. If you previously ran an older build that stored data in `~/.gh-issues-dashboard/`, the server migrates it automatically on first start. +Tokens and snapshots are persisted under `~/.gitdeck/`. Mission goals, Growth Studio data, and server-side preferences are stored in `~/.gitdeck/gitdeck.sqlite`. If you previously ran an older build that stored data in `~/.gh-issues-dashboard/`, the server migrates it automatically on first start. + +### Extending persisted preferences and mission metrics + +Use `setPreference(scope, key, value)` and `getPreference(scope, key, fallback)` from `src/server/preferenceStore.ts` to persist any JSON-serialisable preference without creating a new schema. Low-level parameterised SQLite helpers are in `src/server/sqlite.ts`. + +To add a Goal metric, add one metadata entry to `GOAL_METRIC_DEFINITIONS` in `src/types/goals.ts` and its resolver to `METRIC_RESOLVERS` in `src/server/goals.ts`. The type, creation UI, persistence, progress UI, and AI context update without further wiring. ### GitLab accounts @@ -218,10 +312,14 @@ With Docker Compose (recommended): ```bash cat > .env <<'EOF' GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx -# Optional — enables AI-generated daily digest narratives +# Optional — enables AI-generated digest and Growth Studio narratives (any one provider) OPENAI_API_KEY=sk-... +# ANTHROPIC_API_KEY=... +# GEMINI_API_KEY=... +# OPENROUTER_API_KEY=... # Optional overrides -# OPENAI_DIGEST_MODEL=gpt-4.1-mini +# AI_PROVIDER=openrouter +# AI_MODEL=anthropic/claude-sonnet-5 # GITHUB_OAUTH_SCOPES=repo read:org project read:user user:email EOF docker compose up -d --build @@ -240,7 +338,7 @@ docker run -d --name gitdeck \ gitdeck ``` -The container forwards `GITHUB_CLIENT_ID`, `GITHUB_OAUTH_SCOPES`, `OPENAI_API_KEY` and `OPENAI_DIGEST_MODEL` from the host environment (or `.env` with Compose) — see [Configuration](#configuration) for the full list. It sets `HOST=0.0.0.0` so the server is reachable from outside. To wipe the stored token (full logout) remove the volume: `docker volume rm gitdeck-data`. +The container forwards `GITHUB_CLIENT_ID`, `GITHUB_OAUTH_SCOPES` and the AI variables (`AI_PROVIDER`, `AI_API_KEY`, `AI_MODEL`, `AI_BASE_URL`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `OPENROUTER_API_KEY`) from the host environment (or `.env` with Compose) — see [Configuration](#configuration) for the full list. It sets `HOST=0.0.0.0` so the server is reachable from outside. To wipe the stored token (full logout) remove the volume: `docker volume rm gitdeck-data`. ## Test & type-check diff --git a/docker-compose.yml b/docker-compose.yml index 4185829..a6377b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,8 +9,15 @@ services: environment: GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:?set GITHUB_CLIENT_ID in env or .env} GITHUB_OAUTH_SCOPES: ${GITHUB_OAUTH_SCOPES:-} + AI_PROVIDER: ${AI_PROVIDER:-} + AI_API_KEY: ${AI_API_KEY:-} + AI_MODEL: ${AI_MODEL:-} + AI_BASE_URL: ${AI_BASE_URL:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENAI_DIGEST_MODEL: ${OPENAI_DIGEST_MODEL:-} + OPENAI_MODEL: ${OPENAI_MODEL:-} + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + GEMINI_API_KEY: ${GEMINI_API_KEY:-} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} volumes: - gitdeck-data:/home/node/.gitdeck diff --git a/docs/images/growth-studio-home-dark-desktop.webp b/docs/images/growth-studio-home-dark-desktop.webp new file mode 100644 index 0000000..b430558 Binary files /dev/null and b/docs/images/growth-studio-home-dark-desktop.webp differ diff --git a/docs/images/growth-studio-home-light-mobile.webp b/docs/images/growth-studio-home-light-mobile.webp new file mode 100644 index 0000000..116db56 Binary files /dev/null and b/docs/images/growth-studio-home-light-mobile.webp differ diff --git a/docs/images/growth-studio-queue-dark-mobile.webp b/docs/images/growth-studio-queue-dark-mobile.webp new file mode 100644 index 0000000..4c5fd8e Binary files /dev/null and b/docs/images/growth-studio-queue-dark-mobile.webp differ diff --git a/docs/images/growth-studio-queue-light-desktop.webp b/docs/images/growth-studio-queue-light-desktop.webp new file mode 100644 index 0000000..2fcfa45 Binary files /dev/null and b/docs/images/growth-studio-queue-light-desktop.webp differ diff --git a/docs/images/growth-studio-review-light-mobile.webp b/docs/images/growth-studio-review-light-mobile.webp new file mode 100644 index 0000000..2aa1e7e Binary files /dev/null and b/docs/images/growth-studio-review-light-mobile.webp differ diff --git a/docs/images/growth-studio-unified-calendar-dark-desktop.webp b/docs/images/growth-studio-unified-calendar-dark-desktop.webp new file mode 100644 index 0000000..8488831 Binary files /dev/null and b/docs/images/growth-studio-unified-calendar-dark-desktop.webp differ diff --git a/growth-studio-project/.gitignore b/growth-studio-project/.gitignore new file mode 100644 index 0000000..cc17739 --- /dev/null +++ b/growth-studio-project/.gitignore @@ -0,0 +1 @@ +.runtime/ diff --git a/growth-studio-project/README.md b/growth-studio-project/README.md new file mode 100644 index 0000000..40a6fa4 --- /dev/null +++ b/growth-studio-project/README.md @@ -0,0 +1,89 @@ +# Growth Studio project (ralph loop) + +This directory is the single tracked home for turning GitDeck's Growth Studio +into a first-class, self-contained mode of the application: a dedicated shell +at `/growth`, a repository-centric workspace, a real editorial calendar with +AI-generated, media-complete content, and a measure-and-learn loop. + +Hard constraints (never violated by any task): + +- **The rest of GitDeck keeps working unchanged.** Inbox, repositories, + issues, pull requests, insights, alerts, CI, digests, board and preferences + keep their routes, layout and behavior. Only the `goals` tab is replaced by + the Growth Studio entry. +- **Rules from `AGENTS.md` apply**: English identifiers, comments, docs and + commit messages; pure business logic in `src/utils` with mirrored tests + under `tests/utils`; tests never inside `src`; GitHub API access stays behind + server-side endpoints; no unrelated refactors mixed with feature work. +- **Both locales.** Every new UI string exists in `src/i18n/en.ts` and + `src/i18n/it.ts` (see `docs/translations.md`). +- **Local-first and copy-paste publishing.** No social network credentials are + stored; content is copied (text and image) by the user. Media is mandatory + for a post to reach the `ready` state. +- **Existing goals data is preserved.** Migrations are additive and idempotent. + +## Layout + +- `docs/GS_PLAN.md`: architecture, data model, AI pipeline, phases, standard task loop. +- `docs/GS_FEATURE_MATRIX.md`: authoritative feature inventory and status. +- `docs/GS_DECISIONS.md`: settled decisions with their rationale. +- `tasks/`: task specifications and the authoritative `PROGRESS.md` ledger. +- `scripts/run-gs-tasks.sh`: the ralph loop runner (headless pi or Claude Code sessions). +- `scripts/validate-gs-task.sh`: the local validation gate run after every task. +- `.runtime/`: ignored runner locks, state, logs and gate cache, created on first execution. + +## Run the loop + +```bash +git checkout -b feat/growth-studio # once, from the branch holding the goals work +growth-studio-project/scripts/run-gs-tasks.sh +``` + +With no task IDs, the runner reads `tasks/PROGRESS.md` and starts at the first +`PENDING` task, then continues in lexical order, re-scanning the ledger after +every task so tasks authored mid-run (by phase-opening tasks) are picked up. +Explicit task IDs are supported: + +```bash +growth-studio-project/scripts/run-gs-tasks.sh GS-010 GS-011 +``` + +Each task runs as one non-interactive agent session on the loop branch +(`feat/growth-studio` by default, override with `GS_LOOP_BRANCH`). After the +session the runner re-runs the validation gate itself; a failed gate starts a +focused repair session that amends the same task commit, up to +`GS_LOOP_REPAIR_ATTEMPTS` times (default 3). + +The gate runs `npm run typecheck`, `npm test` and `npm run build`, plus guards +for whitespace errors, tests placed under `src/`, and the presence of both +locale files. Successful stages are cached for six hours, keyed by a hash of +all relevant tracked and non-ignored inputs; any source, test, lockfile or +config change forces the checks to run again. Set `GS_VALIDATION_CACHE=0` to +force every check, or configure the lifetime with `GS_VALIDATION_CACHE_TTL`. + +## Telegram notifications + +```bash +export TELEGRAM_BOT_TOKEN=... +export TELEGRAM_CHAT_ID=... +``` + +Start, finish, repair and failure notifications carry the global completed-task +percentage from `tasks/PROGRESS.md`, prefixed with `[gitdeck-gs]`. Both +variables must be set together; dry runs never send external notifications. + +## Options + +``` +--agent AGENT CLI agent running the sessions: pi or claude (default: pi) +--model MODEL Model override for the selected agent +--thinking LEVEL Thinking level, pi agent only (default: high) +--live / --no-live Stream agent activity to the terminal (default: auto by TTY) +--no-notify Disable desktop notifications +--dry-run Print the sessions that would run +--force Run tasks even when PROGRESS.md says COMPLETED +``` + +Defaults can also be set via `GS_LOOP_AGENT`, `GS_LOOP_MODEL`, +`GS_LOOP_THINKING`, `GS_LOOP_BRANCH`, `GS_LOOP_LIVE`, `GS_LOOP_REPAIR_ATTEMPTS`, +`GS_LOOP_MAX_TASKS`, `GS_VALIDATION_CACHE` and `GS_VALIDATION_CACHE_TTL`. diff --git a/growth-studio-project/docs/GS_DECISIONS.md b/growth-studio-project/docs/GS_DECISIONS.md new file mode 100644 index 0000000..a691430 --- /dev/null +++ b/growth-studio-project/docs/GS_DECISIONS.md @@ -0,0 +1,68 @@ +# Growth Studio decisions + +Settled decisions. Tasks append new rows when they must settle something the +plan leaves open; they never reopen an existing row. + +| ID | Date | Decision | Rationale | +|---|---|---|---| +| D-001 | 2026-09-04 | The repository is the Growth Studio entry point; goals are the Missions panel of a repository workspace. Content items link to a repository and optionally to one or more goals. | A release thread serves stars, forks and downloads at once; a plan makes sense without a numeric target; the unified calendar needs repository-level ownership of content. | +| D-002 | 2026-09-04 | Growth Studio is a separate shell at `/growth`, opened from the main menu with `target="_blank"` and `rel="noopener"`. It has its own top bar and sidebar and renders no dashboard filters, tab strip or footer. | The user wants a focused tool that hides unrelated parts and can sit next to the dashboard in another window. | +| D-003 | 2026-09-04 | Publishing is copy-paste. Every post reaching `ready` must carry at least one media attachment; the UI offers copy text, copy image to clipboard and download image. The application never posts to a network. | No credentials to store, no platform approvals; media-complete posts are the value the user asked for. | +| D-004 | 2026-09-04 | A unified calendar across repositories is in scope, with a colour per repository and multi-repository deconfliction at plan time. | Maintainers of several projects plan as one person. | +| D-005 | 2026-09-04 | Generated image cards are SVG templates produced by the server and rasterized in the browser on a canvas. No native image dependency is added. | Keeps the Docker image and install small and avoids platform-specific builds. | +| D-006 | 2026-09-04 | The task loop mirrors the Emailchef `web-ui-ng-project` runner: `GS-NNN` tasks, `PROGRESS.md` ledger, gate, repair sessions, Telegram notifications. Default agent is pi. Commits carry no `Co-Authored-By` trailer. | Proven structure; pi is the preferred agent; user preference on trailers. | +| D-007 | 2026-09-04 | Interventions and content items become first-class SQLite rows; the legacy JSON in `repository_goals.suggestions` is migrated once and no longer written. | Statuses, dates and attribution need rows, not blobs. | +| D-008 | 2026-09-04 | Plan generation separates slots (deterministic, from cadence and pillars) from angles (AI, one call per plan) from drafts (AI, on demand or batch). | Cheap to regenerate, reorderable before writing, testable without AI. | +| D-009 | 2026-09-04 | GS-010 and GS-012 use one React root and keep `I18nProvider`, `AccountProvider`, and `BrowserRouter` in their current order around a route-aware root selector. Only `/growth` and the `/growth/` segment select `GrowthStudioApp`; `/growth-old` does not. GS-012 handles `/goals` with a client-side replace navigation to `/growth` before either application UI renders. | The providers already wrap the whole application, so a selector inside `BrowserRouter` shares their state, follows client navigation, avoids a dashboard flash, and leaves every other route on `App`. | +| D-010 | 2026-09-04 | GS-011 keeps dashboard authentication ownership unchanged. `GrowthStudioApp` mirrors `App`'s `checking`, `anonymous`, and `authenticated` flow with `fetchAuthStatus`, renders the existing `AuthGate`, refreshes `AccountContext` after authentication, and reuses `logoutAuth` and `AccountSwitcher` for sign-out and account management. | Authentication state is local to `App`, while `AccountContext` only owns the account roster. Extracting a new global auth provider would risk changing dashboard behavior; reusing the existing API and gate gives the shell identical behavior without that refactor. | +| D-011 | 2026-09-04 | In every Growth Studio data hook, the active account ID is part of the load dependency and stale requests are aborted. A switch or removal performed through `AccountSwitcher` clears shell-owned data and navigates to `/growth`; the initial account load does not redirect a deep link. No new cross-window synchronization mechanism is introduced. | `AccountContext.switchAccount` already activates the server account, invalidates the client cache, and refreshes context. Returning the switching window home prevents repository data from the previous account remaining visible, while preserving existing multi-window behavior. | +| D-012 | 2026-09-04 | GS-011 uses the existing `gh-dash.theme` preference and `document.documentElement.dataset.theme`; `mode-growth` is added on shell mount and removed on unmount. The repository switcher derives selection only from validated `owner` and `repo` route params. **Provisional:** options are opening every newly selected repository at Overview or retaining the current repository panel; retain Missions, Interventions, Calendar, Library, or Review when already in a workspace, and use Overview when switching from a global screen. | Shared theme storage gives immediate dashboard parity. Route-derived repository state makes deep links authoritative. Retaining the panel is the recommended switch behavior because it supports comparing the same workflow across repositories without inventing a second selection state. | +| D-013 | 2026-09-04 | GS-012 removes only dashboard ownership of goals: `GoalsView` and all goal API functions remain available to Growth Studio. The Growth Studio anchor is rendered separately from the dashboard tab-button collection, with no count or active state, and `/goals` remains server-recognized solely as the legacy client redirect. | This accounts for the direct consumers in plan section 4.2 while preserving stored goals and the Missions implementation. A separate anchor avoids weakening the dashboard `Tab` type to accommodate a non-tab destination. | +| D-014 | 2026-09-04 | GS-013 extends `GoalsView` with an optional fixed-repository mode rather than copying it. In that mode the repository is initialized from the validated route, displayed as read-only, omitted from user selection, and both the supplied goals and repositories are scoped to that exact name. AI preferences open `/preferences#preferences-ai` in a new window with noopener behavior. | The current component owns all create, delete, advice, source, and proposal behavior. A narrow mode keeps one implementation, prevents cross-repository creation from a workspace, and respects the separate-shell boundary. | +| D-015 | 2026-09-04 | GS-014 initially builds Home from `fetchGoals` and `fetchRepos`, preserves a fallback identity for a goal whose repository is absent from the repository response, and excludes represented repositories from the starter picker. **Provisional:** options for profile-only repositories are client fan-out over every repository or one account-scoped summary endpoint; GS-022 should add `GET /api/growth/workspaces` and GS-014 should adopt it when available. | Phase 1 has no profile API, so goals are the only persisted growth signal it can read. A single summary endpoint is the recommended final source because fan-out is slow and cannot distinguish a default profile response from a persisted profile. | +| D-016 | 2026-09-04 | GS-015 defines growth transport entities in camelCase, with decoded JSON fields, numeric booleans, and ISO date strings. Its `src/api/growth.ts` placeholder returns a typed zero-valued `GrowthWorkspaceSummary` without a network request; repository identity and goal progress still come from existing repository and goal fetchers. GS-022 replaces that function body without changing panel callers. | This fixes the client contract before persistence exists, avoids requesting an endpoint that phase 1 has not registered, and keeps SQLite column naming out of React components. | +| D-017 | 2026-09-04 | GS-016 treats the shell sidebar as fixed at 960 pixels and above and closed by default below that breakpoint; the top-bar control opens it, while navigation, backdrop activation, and Escape close it. GS-011 also replaces the hard-coded `RepositoryPicker` summary and fallback strings with locale keys in both locales. The README uses a non-broken textual screenshot placeholder until a real release screenshot exists. | These rules mirror the existing responsive and dismissible-control patterns and ensure the shared picker does not introduce English-only shell text. A textual placeholder avoids committing a dead image link. | +| D-018 | 2026-09-04 | GS-020 creates all six tables and indexes in one idempotent schema initializer. IDs are server-generated UUIDs; timestamps are UTC ISO strings. SQLite checks constrain enumerated values, internal plan and intervention references use `ON DELETE SET NULL`, performance rows use `ON DELETE CASCADE`, and `goal_id` has no foreign key because `repository_goals` is lazily created. Every query still scopes explicitly by `account_id`. | This follows the existing SQLite helper, which enables foreign keys, without making growth schema initialization depend on the goals table or an account table. Explicit account predicates remain the authorization boundary. | +| D-019 | 2026-09-04 | **Provisional:** profile defaults could be inserted by a read or returned without persistence; GS-020 returns defaults without inserting and only an upsert creates a growth profile. Defaults are language `en`, UTC timezone, empty voice, audience, hashtags, avoid text, and posting windows, the three legacy social channels enabled, other channels disabled, cadence X 3, LinkedIn 1, Mastodon 3, five stable pillar IDs each weighted 20, and a deterministic repository colour from a fixed accessible palette. | Non-persisting reads are recommended so merely viewing a repository does not make it appear configured on Home. Stable complete defaults make later planning and profile editing deterministic. | +| D-020 | 2026-09-04 | **Provisional:** content status handling could enforce a strict transition graph or only state invariants; GS-020 enforces enum validity plus invariants and does not impose a transition graph. `ready`, `scheduled`, and `published` require media; `scheduled` also requires a valid `scheduled_for`; marking published sets `published_at` in UTC and accepts only an optional HTTP or HTTPS URL. Clearing a schedule moves a scheduled item back to `ready`. | Invariant-only enforcement is recommended because phase 2 needs editorial corrections and no approved workflow graph exists. The explicit media and date rules enforce settled publishing requirements without blocking later UI actions. | +| D-021 | 2026-09-04 | GS-021 runs migration per account in a transaction and records `growth` preference key `migratedSuggestionsV1:` only after success. Suggestions map in stored order to AI-origin proposed interventions; each proposal maps to one linked draft, with formats `x-thread`, `linkedin-post`, and `mastodon-post` mapped to channels `x`, `linkedin`, and `mastodon`, and all other legacy formats mapped to `other`. Media suggestions become URL media with their title as alt text and guidance as caption. | The account-qualified marker matches account-scoped storage and makes retries safe. One transaction prevents a partial migration plus marker, while an explicit catch-all channel preserves every legacy proposal format. | +| D-022 | 2026-09-04 | **Provisional:** regenerated goal advice could replace old interventions or accumulate duplicate backlog rows; generation upserts by account, repository, goal, and normalized title, updating copy while preserving status, and compatibility projection orders interventions by creation time and ID. Goal reads project interventions and linked content back to legacy suggestions until GS-023; the JSON column remains untouched. Deleting a goal detaches its interventions and content goal references instead of deleting growth work. | Upsert-and-preserve is recommended because intervention statuses are user decisions and should survive regeneration. Stable projection keeps the index-based proposal route usable during transition, and detaching honors optional goal links. | +| D-023 | 2026-09-04 | GS-022 APIs use `{ ok: true, ... }` success envelopes and the existing `{ ok: false, error }` failure shape. Route repositories are reconstructed from separately decoded `owner` and `repo` segments and validated with `parseRepositoryName`; cross-account IDs return 404. **Provisional:** PATCH could accept replacement documents or allowlisted partial fields; use allowlisted partial fields, reject unknown or empty patches, and keep profile PUT as a complete normalized replacement. | This follows the goals routes and prevents account existence leaks or mass assignment. Partial PATCH is the recommended fit for drawer actions, while complete profile PUT gives normalization one unambiguous input. | +| D-024 | 2026-09-04 | **Provisional:** repository intervention generation could run once per goal or once for the repository; `POST /api/growth/interventions/generate` runs once for repository signals and accepts an optional validated `goalId`. Without it, generated interventions are repository-level even when several goals exist. Generated and deterministic fallback results use origin `ai`, dedupe by account, repository, optional goal, category, and normalized title, and never reset an existing status. | One repository-level call is recommended because interventions may support several missions and D-001 makes the repository the entry point. An optional goal preserves focused generation, and dedupe prevents repeated button presses from rebuilding the backlog. | +| D-025 | 2026-09-04 | GS-023 loads one repository backlog, performs category and origin filtering client-side, and keeps the dismissed group collapsed in component state by default. Manual rows use origin `manual`, have no rule key or goal unless explicitly launched from a mission, and receive the same dedupe handling as generated rows. | The expected repository list is small and already returned for inline grouping, so client filters avoid unnecessary API variants. Local collapse state is presentation state and does not belong in persistence. | +| D-026 | 2026-09-04 | Repeated intervention drafting returns the complete current-generation social set when it exists. A requested refresh updates only an existing `idea` or `draft` for each format and creates a new draft rather than overwriting ready, scheduled, published, or skipped work. One proposal becomes one content item; goal IDs contain the intervention goal when present. Goal-less drafting passes repository context and signals through `generateGoalProposals`, and unavailable AI returns the existing not-configured error rather than fabricated copy. | Idempotent retries prevent duplicate editable drafts while protecting reviewed or published work. The mapping matches legacy proposal generation and leaves deterministic planner and drafter fallbacks to phase 3. | +| D-027 | 2026-09-04 | **Provisional:** posting weekdays could use JavaScript numbering or ISO numbering; profiles use ISO weekdays 1 through 7 for Monday through Sunday and integer local hours 0 through 23 interpreted in the profile IANA timezone. Pillar IDs are unique non-empty slugs, each weight is 0 through 100 without requiring a total of 100, at least one pillar has positive weight, channels are the six planned values, cadence is 0 through 14, hashtags are trimmed and deduplicated case-insensitively, and colours are `#RRGGBB`. | ISO weekdays are the recommended human scheduling convention and avoid locale-dependent labels in storage. Relative weights can be normalized by the planner, while the remaining constraints make profile round-trips deterministic and safe. | +| D-028 | 2026-09-04 | GS-026 authors phase 3 tasks against the names and API contracts actually shipped by GS-020 through GS-025, records deviations before authoring, and makes no phase 3 source changes. New task rows remain `PENDING` and each task owns one independently verifiable vertical slice. | Phase-closing work is governance, not early implementation. Using shipped contracts prevents future tasks from referencing aspirational helpers that do not exist. | +| D-029 | 2026-09-04 | The phase 2 implementation matches its planned architecture with three phase 3 extension points: `content_plans` has create/archive store operations but no list or API, `growth_assets` has schema and types but no CRUD, and `POST /api/growth/content/draft` is intervention-specific. Phase 3 adds plan reads/routes, a read-only asset query for drafting, and a distinct item route at `POST /api/growth/content/:id/draft` without renaming shipped contracts. | This records the actual seams found at closure and lets phase 3 extend tested clients and routes without breaking the Interventions workflow or implementing phase 4 uploads early. | +| D-030 | 2026-09-04 | Phase 3 content plans cover one to four complete ISO weeks, Monday through Sunday, and generated plans are active. An account and repository cannot have overlapping active plans. Planner CTA text is stored in the existing content-item `summary`; deterministic fallback replaces unavailable or unusable AI output, while a configured-provider transport failure creates no partial plan. | Complete weeks make cadence counts exact and calendar behavior deterministic. Reusing `summary` avoids an unplanned schema change, and atomic generation keeps the editorial calendar recoverable. | +| D-031 | 2026-09-04 | Month, week, and queue are views of the existing repository calendar route selected by the `view` query parameter; Queue gets no new sidebar route. Dragging an idea or draft changes its date while preserving status. Phase 3 ICS accepts an optional repository for future unified use but exports only items already in `scheduled` status. | One calendar surface preserves the settled shell navigation. Status preservation prevents rescheduling from bypassing media review, while the optional filter avoids redesigning the export endpoint in phase 6. | +| D-032 | 2026-09-04 | GS-030 sorts and deduplicates posting windows, spreads multi-slot channel cadence from the first through last available window, and uses Monday-forward even spacing at 10:00 local time when no windows exist. Pillars use smooth weighted round-robin allocation over the final chronological slot order, preserving profile order for ties and excluding non-positive weights. IANA timezone conversion normalizes daylight-saving gaps forward and selects the post-transition occurrence for overlaps. | The task fixed determinism and counts but left exact distribution and DST disambiguation open. These rules use the full configured week, avoid input-order drift and long runs of one pillar, and remain stable without randomness. | +| D-033 | 2026-09-04 | GS-031 accepts at most one usable AI assignment per stable slot key, constrains confirmed pillars to positive-weight profile pillars and cited URLs to collected evidence, and deterministically fills every missing or unusable slot. `usedFallback` is true when AI is unavailable or at least one slot needs a fallback. Repository plan lists are ordered by newest period first. | Partial structured answers should preserve valid work without leaving calendar holes, while evidence allowlisting prevents fabricated citations. Newest-first plan ordering supports the management UI planned for GS-037. | +| D-034 | 2026-09-04 | GS-032 returns an already populated generated draft as cached unless `refresh` is true. A configured provider's platform-invalid answer fails without mutating the item; deterministic copy is reserved for unavailable AI. Stored assets precede README and additional-source media candidates, and the first candidate is attached when the model omits a usable selection. | Caching protects editorial edits and avoids unnecessary calls, while explicit refresh remains predictable. Rejecting invalid configured output surfaces provider quality problems instead of silently replacing requested AI copy, and deterministic media ordering moves drafts toward the mandatory-media gate without accepting invented URLs. | +| D-035 | 2026-09-04 | Calendar rescheduling uses native HTML drag and drop plus Alt with the left or right arrow key for an equivalent keyboard move by one local calendar day. Targets are limited to rendered date cells, the item's local wall-clock time is preserved with D-032 daylight-saving disambiguation, and PATCH explicitly carries its current editorial status. Week guides show the profile's unique posting hours without snapping actual item times. | Dependency-free interactions satisfy pointer and keyboard workflows. Explicit status prevents an idea or draft from being promoted past media review, while local-time conversion and non-snapping guides keep the configured publishing timezone authoritative. | +| D-036 | 2026-09-04 | The editorial Queue maps dated `idea` items to Needs draft and keeps the remaining non-skipped statuses as explicit workflow sections. Draft details runs the existing item drafter and opens its result in `ContentItemDrawer`; publishing only records a user-completed publication through the existing endpoint. | This makes plan ideas actionable from the weekly workflow while preserving one editing surface, the mandatory-media server gate, and the copy-paste-only publishing boundary. | +| D-037 | 2026-09-04 | Plan lifecycle POST actions accept only an empty JSON object. Regeneration returns the archived `sourcePlan` and its `affectedContentItems` separately from the replacement `plan` and `contentItems`; archive returns its updated `plan` and affected `contentItems`. The manager defaults to the complete ISO week strictly after the current week and offers lifecycle actions only for non-archived plans. | Explicit response roles let clients reconcile both generations without guessing, empty action bodies prevent future mass assignment, and keeping archived plans read-only avoids accidentally regenerating superseded history while an active replacement overlaps it. | +| D-038 | 2026-09-04 | Phase 3 shipped the planned slot, planner, item-drafter, month, week, queue, ICS, and lifecycle contracts without a schema change. The phase 4 seams are the existing `growth_assets` table and `GrowthAsset` type, a repository-scoped `listGrowthAssets` store read used by the drafter, exported README and SSRF-guarded website signal readers, asset-ID media that currently has no browser resolver, and read-only media sections in Queue and `ContentItemDrawer`. | Recording the actual extension points keeps phase 4 tasks from duplicating signal collection, replacing content routes, or changing the phase 3 editorial workflow while adding media persistence and controls. | +| D-039 | 2026-09-04 | Phase 4 serves every asset through authenticated `GET /api/growth/assets/:id/file` and never exposes a stored filesystem path. Uploads use bounded raw request bodies with allowlisted metadata, supported PNG, JPEG, WebP, GIF, MP4, and WebM types, a 25 MiB limit, and UUID filenames below `DATA_DIR/growth-assets/`. Imported assets remain remote URL references that are revalidated and proxied through the shared SSRF guard; generated assets persist only validated card template data and are rendered as safe SVG. Image-to-PNG rasterization is always a bounded 2x browser canvas operation, and content attachment validates asset account and repository ownership. | One protected asset URL works for uploads, remote imports, generated cards, previews, drafting, and clipboard actions. Raw uploads avoid multipart dependencies, URL revalidation prevents the server becoming an SSRF proxy, metadata-only cards honor client-side rasterization, and ownership validation closes the existing arbitrary asset-ID seam. | +| D-040 | 2026-09-04 | GS-042 canonicalizes discovered media URLs, keeps the first source when several sources cite the same URL, and deduplicates imports per account and repository. A repeated import returns the existing private asset with `duplicate: true` and does not download it again; every later file request still revalidates and downloads the remote media through the bounded SSRF guard. | Stable first-source ownership keeps candidate origin labels deterministic, while an explicit idempotent response supports duplicate feedback without unnecessary transfer and authenticated proxy reads still enforce current network safety. | +| D-041 | 2026-09-04 | Generated cards persist exact bounded template data: release version plus one to four highlights, a non-negative integer milestone plus label and detail, one to four finite numeric stats, a quote plus attribution, or one to five what's-new items. Cards are always 1200 by 675 and use the repository's current profile colour when the authenticated SVG file is requested. | Exact shapes reject unknown or executable metadata, keep deterministic layouts bounded, and avoid a colour schema snapshot while letting a repository's reusable cards remain visually aligned with its profile. | +| D-042 | 2026-09-04 | Browser rasterization accepts the four persisted raster image formats plus generated SVG, requires the caller to choose white or transparent output, and uses an exact 2x size only when it remains within an 8192-pixel edge and 32-megapixel canvas; oversized input is rejected rather than silently downscaled. | Matching persisted image formats keeps media behavior predictable, an explicit background preserves caller intent, and fixed limits bound canvas memory while rejection avoids claiming a result is 2x when it is not. | +| D-043 | 2026-09-04 | GS-045 preserves image transparency when preparing social PNGs. Asset-backed media bytes are loaded only through the authenticated asset file fetcher, while legacy HTTP or HTTPS images are fetched directly. Missing image clipboard support or a rejected write triggers an immediate PNG download; preparation failures keep the explicit download control and, for legacy media, an external source link. | Transparent output preserves uploaded artwork without inventing a background. The authenticated asset boundary prevents stored paths or remote asset URLs from leaking, and the legacy source link is the only reliable browser fallback when CORS prevents client-side rasterization. | +| D-044 | 2026-09-04 | Phase 4 shipped against the existing `growth_assets` table with private local, remote, and generated serving; all five cards remain metadata-only SVG; attachment validation is enforced on create and patch; and rasterization plus clipboard fallback remain browser-only. Phase 5 extends the existing `content_performance` table, `evergreen` flag, rule-origin interventions, repository signals, planner, and placeholder Review routes without replacing these contracts. | The closure audit found the planned media seams complete and no schema or dependency deviation. Recording the actual extension points prevents the learning loop from duplicating asset, content, or route ownership. | +| D-045 | 2026-09-04 | Opportunity scans are explicit account-scoped repository POST actions. They upsert `origin='rule'` interventions by a stable `rule_key` while preserving user-selected status. Rules cover a release at least three days old with no source-linked published post, the next deterministic star milestone within five percent, unassigned and uncommented good-first issues older than 14 days, merged pull requests with at least 500 changed lines or 20 files and no source-linked published post, active goals more than five percentage points behind elapsed pace, and published evergreen content at least 60 days old. Release and merged-PR lookback is 30 days. | Stable rule identity avoids duplicate backlog rows when dynamic copy changes, explicit scans avoid hidden forge traffic, and bounded windows and thresholds make every rule deterministic and testable. | +| D-046 | 2026-09-04 | Snapshot attribution uses the nearest retained UTC-day repository snapshot on or before publication and the first snapshot on or after the 48-hour or 7-day deadline, requiring each boundary to be within one UTC day of the requested point. It persists signed star and fork deltas only when both boundaries exist, upserts one account-scoped row per content item and window, and leaves unavailable windows pending without extrapolation. | The shipped snapshot history contains daily stars and forks but no closed-PR or download history. Tight boundary tolerance avoids presenting a long data gap as a 48-hour or 7-day effect while allowing ordinary daily collection jitter. | +| D-047 | 2026-09-04 | Performance summaries keep 48-hour and 7-day windows separate and aggregate account-scoped published items by channel and pillar without double-counting the two windows. Weekly Growth Review is a read-only projection of the previous complete UTC ISO week plus the current upcoming week. Its metrics, misses, and three next-action recommendations are deterministic; optional AI may replace only the narrative, and every AI error falls back to deterministic copy. | Separate windows remain interpretable, a complete UTC week is consistent for repository and global review, and keeping AI away from measured facts makes the review useful and reproducible when no provider is configured. | +| D-048 | 2026-09-04 | Evergreen recycling is user-triggered from a stable `evergreen:` rule intervention. One eligible published source creates at most one linked `idea`, preserving the source row and carrying repository, channel, format, pillar, goals, and evidence URLs while clearing draft copy, schedule, media, plan ownership, and the evergreen flag. | Linking the recycled idea to the deduplicated rule intervention needs no schema change, protects published work, prevents repeated clicks from multiplying ideas, and forces refreshed copy and media through the normal editorial workflow. | +| D-049 | 2026-09-04 | Plan re-weighting uses complete 7-day performance from the eight weeks before a plan, and activates only with at least three measured items across at least two positive-weight pillars. Each pillar multiplier is clamped from 0.8 through 1.2, zero-weight pillars stay zero, integer weights are normalized back to the configured positive total, and only the plan snapshot is changed; the persisted profile is never mutated. | A minimum sample and bounded integer redistribution permit learning without allowing one noisy post to rewrite strategy or violating the existing profile and slot contracts. | +| D-050 | 2026-09-04 | Star opportunities use the next 1-2-5 milestone at or above ten and trigger when the current count is within five percent. Rule keys are `release:`, `star-milestone:`, `good-first-issue:`, `merged-pr:`, `goal-pace:`, and `evergreen:`; results keep that rule-family order and sort naturally within a family. | D-045 fixed thresholds and stable identity but not milestone steps or serialized keys. The 1-2-5 sequence remains useful across repository sizes, while repository-scoped natural keys make repeated scans deterministic and inspectable. | +| D-051 | 2026-09-04 | Performance refresh recomputes every due window in the requested account and repository scope, returns all persisted rows in that scope, and reports each incomplete window as pending with a missing-publication, not-due, or snapshot-unavailable reason. A failed boundary lookup never deletes an earlier measurement, and repositories with no due windows do not read snapshot history. | The task required pending work and idempotent refreshes but did not define their transport representation. Returning retained measurements alongside explicit pending reasons lets clients distinguish unavailable history from unmeasured work while avoiding unnecessary server-side reads. | +| D-052 | 2026-09-04 | Performance summaries always return 48-hour then 7-day windows with zeroed known metrics, order measured channel groups by the supported channel sequence, and order pillar IDs alphabetically with the collision-safe `__unassigned__` fallback last. Additional finite metrics are retained in stable key order, and a duplicate content-window input uses its latest measurement without increasing the measured-item count. | Stable machine keys let later Review UI localize missing pillars without storing display text, while fixed ordering and duplicate handling make summary responses reproducible and preserve future metrics without weakening the current four-metric contract. | +| D-053 | 2026-09-04 | A weekly Review miss is an item scheduled in the previous complete UTC week without a publication in that same week, so skipped and late-published work remains visible; upcoming work is scheduled in the current UTC week and not published or skipped. Each attribution window selects its strongest channel and pillar by combined finite delta, measured count, then stable key. Recommendations prioritize recovery, measured channel and pillar follow-ups, upcoming preparation, active interventions, and missing measurement before filling to exactly three bounded baseline actions. | These definitions expose late execution instead of rewriting history, preserve D-047's separate windows, and make empty or sparse reviews actionable and reproducible without fabricating metrics or persisting recommendation rows. | +| D-054 | 2026-09-04 | GS-056 scores each eligible item as its signed star delta plus fork delta, averages scores per pillar, centers them on the eligible all-item mean, and linearly scales the greatest absolute deviation to the 0.8 through 1.2 multiplier bounds. The publication lower boundary eight weeks before the plan is inclusive, the plan boundary is exclusive, and the 7-day measurement must also exist before the plan. Normalized integer remainders go to the largest fractional part, then profile order. | Centering rewards relative performance even when every result is negative, per-item pillar averages prevent posting volume alone from winning, strict temporal boundaries avoid future leakage, and stable largest-remainder allocation preserves the configured positive total reproducibly. | +| D-055 | 2026-09-04 | Phase 5 shipped without a schema change: explicit repository scans upsert rule interventions while preserving status; attribution persists only the retained snapshot metrics for stars and forks and reports other work as pending; summaries and repository or global Review are read-only; recycling is an explicit deduplicated action; and re-weighting changes only plan pillar snapshots. Phase 6 extends the existing account-wide `listContentItems`, optional-repository `listContentPlans`, repository-optional ICS export, `getGrowthProfile` defaults, and JSON `preferenceStore`; `/growth/calendar` and `/growth/settings` remain placeholders, profiles remain repository-scoped, and `generateGrowthContentPlan` remains single-repository. | The closure audit found the learning-loop contracts complete and identified the exact reusable seams and remaining global placeholders. Recording them prevents unified calendar, settings, and multi-repository work from replacing stable repository routes, duplicating persistence, widening forge access, or reopening phase 5 behavior. | +| D-056 | 2026-09-04 | The unified calendar transport nests visible content items under independently ordered repository entries carrying colour, timezone, posting windows, and ordered pillar ID-label pairs. It includes empty persisted-profile repositories, uses deterministic profile defaults for content-only repositories, and keeps the requested UTC bounds as inclusive query inputs rather than converting items to a shared calendar date. | Repository nesting prevents repositories in different timezones from losing their local date semantics, supplies stable filter metadata for GS-061, and keeps one bounded read compatible with the existing account-wide content and profile stores. | +| D-057 | 2026-09-04 | GS-061 requests the rendered local-date range through one universal UTC envelope covering offsets from UTC+14 through UTC-12, then excludes items outside each owning profile's local range. Repository, channel, repository-qualified pillar, and status filters remain in the URL; equal pillar IDs use `repository::pillarId` values. Rescheduling and item time formatting always use the owning repository profile, and account-wide ICS uses the same visible-range envelope without a repository filter. | A single bounded request cannot know profile timezones before its response; the universal envelope includes every instant that can belong to a rendered local day. Repository-qualified pillar values avoid label and selection collisions, while preserving profile-local editing and the existing ICS transport. | +| D-058 | 2026-09-04 | Multi-repository deconfliction processes repositories case-insensitively in stable name order and then stable slot-key order. For every repeated pillar and repository-local date, the later slot moves to the nearest unused date anywhere in the shared complete-week period, preferring forward on equal distance, while preserving its profile timezone, local wall-clock time, channel, format, and deterministic pillar. A collision remains only after every period date for that pillar is occupied and is reported separately. All repository profiles, performance weights, slots, signals, and configured AI assignments are prepared before one account-scoped persistence transaction; any overlap, provider error, or database error creates none of the requested plans. | Global stable ordering makes identical requests reproducible, full-period nearest-date movement maximizes separation without changing cadence, and retaining deterministic slot pillars prevents AI confirmation from invalidating the stagger. Preflight plus one transaction prevents a successful repository from leaking partial plan IDs or work when another repository fails. | +| D-059 | 2026-09-04 | Growth-wide settings use preference scope `growth` and the account-qualified key `settings:`. Reads validate the complete stored document and fail closed to copied built-in defaults without inserting; saves normalize and replace only that account's document, and reset deletes only that key. | Account qualification provides isolation without a new table, while validating on every read makes corrupt preference JSON harmless and keeps repository profiles independent until GS-064 applies settings to unpersisted defaults. | +| D-060 | 2026-09-04 | `getGrowthProfile` applies the active account's current Growth timezone, cadence, and ordered pillars only when no profile row exists, returning defensive copies without materializing a row. Persisted rows always remain authoritative, including their built-in JSON fallbacks, and settings saves or resets do not optimistically rewrite open repository profile state. | Read-time inheritance makes new repositories immediately reflect account defaults while preserving repository-specific choices, deterministic colours, sentinel metadata, account isolation, and non-persisting profile reads. | +| D-061 | 2026-09-09 | Final Growth API reads and actions reject unknown query fields and repeated single-value fields; private asset bytes remain authenticated and account-scoped. Release dialogs and drawers move focus inside, dismiss with Escape, and restore their opener. The dependency floor must produce zero `npm audit` findings without adding a production native or outbound-posting dependency. | Strict transport input avoids ambiguous filters and action URLs, focus lifecycle closes the keyboard accessibility gaps found during release QA, and dependency upgrades remove disclosed router, build, test, and HTTP-client advisories without changing the settled copy-paste publishing or asset boundaries. | diff --git a/growth-studio-project/docs/GS_FEATURE_MATRIX.md b/growth-studio-project/docs/GS_FEATURE_MATRIX.md new file mode 100644 index 0000000..0e3eb14 --- /dev/null +++ b/growth-studio-project/docs/GS_FEATURE_MATRIX.md @@ -0,0 +1,63 @@ +# Growth Studio feature matrix + +Authoritative inventory of Growth Studio capabilities. Statuses: `EXISTING` +(present before the project, may move), `PLANNED`, `IN_PROGRESS`, `DONE`, +`DROPPED`. Tasks update the rows they touch. + +| Feature | Location (current or target) | Status | Task | +|---|---|---|---| +| Governance baseline and validation gate | `growth-studio-project/scripts/`, `src/utils/colors.ts`, `tests/utils/colors.test.ts` | DONE | GS-000 | +| Validation guards for test placement and English/Italian locale key parity | `growth-studio-project/scripts/validate-gs-task.sh`, `growth-studio-project/scripts/check-i18n-key-parity.mjs` | DONE | GS-003 | +| Phase 1 and phase 2 implementation decisions | `growth-studio-project/docs/GS_DECISIONS.md` | DONE | GS-002 | +| Goals CRUD with metric refresh (stars, forks, closed PRs, downloads) | `src/components/views/GoalsView.tsx`, `src/api/github.ts`, `src/server/goalStore.ts`, `src/server/goals.ts`, `src/server/routes/goals.ts` | DONE | — | +| AI suggestions per goal (3–5 actions); four deterministic actions when AI is not configured | `src/server/goals.ts` `generateGoalSuggestions`, `src/server/growth/signals.ts`, `src/server/goalStore.ts` compatibility projection, `src/server/growth/store.ts` interventions, `src/components/growth/GrowthInterventions.tsx` | DONE | GS-021 and GS-023 | +| AI proposals per suggestion (X thread, LinkedIn, Mastodon) with source-backed media suggestions when assets are available | `src/server/goals.ts` `generateGoalProposals`, `src/server/growth/signals.ts`, `src/utils/socialProposals.ts`, `src/server/goalStore.ts` compatibility projection, `src/server/growth/store.ts` content items, `src/components/modals/GoalProposalsModal.tsx`, `/api/growth/content/draft` | DONE | GS-021, GS-023, and GS-024 | +| Repository content sources (repositories and websites) with SSRF-guarded website reads during generation | `src/server/goalStore.ts`, `src/server/routes/repository.ts`, `src/server/growth/signals.ts`, `src/api/github.ts`, `src/components/growth/GrowthLibrary.tsx`, `src/components/common/RepositoryContentSources.tsx`, `src/components/common/ContentSourcePicker.tsx`, `src/utils/socialProposals.ts` | DONE | GS-023 and GS-025 | +| AI provider settings and connection test | `src/components/preferences/AiIntegrationSettings.tsx`, `src/api/github.ts`, `src/server/routes/ai.ts`, `src/server/ai/client.ts`, `src/server/ai/settings.ts`, `src/server/ai/providers.ts` | DONE | — | +| `/growth` client routes served by the SPA | `src/server/spa.ts`, `src/main.tsx`, `src/components/growth/GrowthStudioApp.tsx`, `tests/server/spa.test.ts` | DONE | GS-010 | +| Growth shell: own top bar, sidebar, `mode-growth` body class, no dashboard chrome | `src/components/growth/GrowthStudioApp.tsx`, `src/components/growth/GrowthTopBar.tsx`, `src/components/growth/GrowthSidebar.tsx`, `src/styles/growth/shell.css` | DONE | GS-011 | +| Main-menu entry opening `/growth` in a new window; `goals` tab removed; `/goals` redirect | `src/App.tsx`, `src/main.tsx`, `src/utils/dataRequirements.ts`, `src/components/SidebarControls.tsx` | DONE | GS-012 | +| Missions panel hosting the existing goals UI | `src/components/growth/GrowthStudioApp.tsx`, `src/hooks/useGoals.ts`, `src/components/views/GoalsView.tsx` | DONE | GS-013 | +| Growth home: repositories with profiles or goals, profile colours, mission and workflow totals, and global shortcuts | `src/components/growth/GrowthHome.tsx`, `src/utils/growthHome.ts`, `/api/growth/workspaces`, `/growth` | DONE | GS-014, GS-022, and GS-065 | +| Workspace overview per repository | `src/components/growth/GrowthWorkspaceOverview.tsx`, `src/types/growth.ts`, `src/api/growth.ts`, `/api/growth/workspace/:owner/:repo`, `/growth/r/:owner/:repo` | DONE | GS-015 and GS-022 | +| Shell i18n, responsive layout, theme parity, and keyboard dismissal | `src/components/growth/`, `src/components/common/RepositoryPicker.tsx`, `src/styles/growth/`, `src/i18n/en.ts`, `src/i18n/it.ts` | DONE | GS-016 | +| Growth Studio release guide, operations notes, and anonymized screenshot gallery | `README.md`, `docs/images/` | DONE | GS-016 and GS-066 | +| Growth store schema (profiles, interventions, plans, items, assets, performance) | `src/server/growth/store.ts`, `src/types/growth.ts`, `src/utils/growth/profileDefaults.ts`, `tests/server/growthStore.test.ts` | DONE | GS-020 | +| One-shot migration of legacy suggestions and proposals | `src/server/growth/store.ts`, `src/utils/growth/legacySuggestions.ts`, `tests/server/growthStore.test.ts` | DONE | GS-021 | +| Growth API routes and account-wide workspace summaries with effective profile colours | `src/server/routes/growth.ts`, `src/server/growth/store.ts`, `src/api/growth.ts`, `tests/server/growthRoutes.test.ts` | DONE | GS-022 and GS-065 | +| Shared repository signal collection with SSRF-guarded sources | `src/server/growth/signals.ts`, `src/server/snapshots.ts`, `tests/server/growthSignals.test.ts` | DONE | GS-023 and GS-050 | +| Interventions backlog with statuses, AI and fallback generation, dedupe, manual creation, filters, and linked content | `src/components/growth/GrowthInterventions.tsx`, `src/server/routes/growth.ts`, `src/server/growth/store.ts`, `/growth/r/:owner/:repo/interventions` | DONE | GS-023 | +| Content items list and drawer with editing, copy, scheduling, and publication actions | `src/components/growth/ContentItemDrawer.tsx`, `src/components/growth/GrowthInterventions.tsx`, `/api/growth/content/draft`; later reused by Calendar | DONE | GS-024 | +| Library panel: sources, profile (voice, audience, channels), pillars, cadence | `src/components/growth/GrowthLibrary.tsx`, `src/utils/growth/profile.ts`, `/growth/r/:owner/:repo/library`, `/api/growth/profiles/:owner/:repo` | DONE | GS-025 | +| Phase 2 closure and phase 3 task authoring | `growth-studio-project/tasks/GS-030.md` through `GS-038.md`, `growth-studio-project/tasks/PROGRESS.md`, `growth-studio-project/docs/GS_DECISIONS.md` | DONE | GS-026 | +| Deterministic slot builder from cadence, pillars, posting windows | `src/types/growth.ts`, `src/utils/growth/planSlots.ts`, `tests/utils/growth/planSlots.test.ts` | DONE | GS-030 | +| AI planner assigning evidence-grounded angles to slots with deterministic fallback and atomic persistence | `src/server/growth/planner.ts`, `src/utils/growth/planAssignments.ts`, `src/server/growth/store.ts`, `src/api/growth.ts`, `/api/growth/plans`, `/api/growth/plans/generate` | DONE | GS-031 | +| AI drafter producing media-aware drafts per slot | `src/server/growth/drafter.ts`, `src/utils/growth/contentDraft.ts`, `src/utils/growth/mediaCandidates.ts`, `src/server/growth/store.ts`, `src/api/growth.ts`, `/api/growth/content/:id/draft` | DONE | GS-032 | +| Calendar month and week views with drag and drop | `src/components/growth/calendar/`, `src/utils/growth/calendar.ts`, `/growth/r/:owner/:repo/calendar` | DONE | GS-033 and GS-034 | +| Queue "this week" with timezone-local workflow groups, drafting, copy, media previews, and manual publication recording | `src/components/growth/calendar/GrowthQueue.tsx`, `src/utils/growth/calendar.ts`, `/growth/r/:owner/:repo/calendar?view=queue` | DONE | GS-035 | +| Account-scoped ICS export for scheduled content with repository and visible-range filters | `src/utils/growth/ics.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `src/components/growth/calendar/GrowthCalendar.tsx`, `/api/growth/calendar.ics` | DONE | GS-036 | +| Content plan create, regenerate, and archive management | `src/components/growth/calendar/GrowthPlanManager.tsx`, `src/server/growth/planner.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `/api/growth/plans/:id/regenerate`, `/api/growth/plans/:id/archive` | DONE | GS-037 | +| Phase 3 closure and phase 4 task authoring | `growth-studio-project/tasks/GS-040.md` through `GS-047.md`, `growth-studio-project/tasks/PROGRESS.md`, `growth-studio-project/docs/GS_DECISIONS.md` | DONE | GS-038 | +| Account-scoped asset persistence, bounded uploads, and authenticated file serving | `src/server/growth/assets.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `tests/server/growthAssets.test.ts`, `/api/growth/assets`, `/api/growth/assets/:id/file` | DONE | GS-040 | +| Library asset list and uploads | `src/components/growth/GrowthAssetLibrary.tsx`, `src/components/growth/GrowthLibrary.tsx`, `src/utils/growth/assets.ts`, `src/styles/growth/library.css`, `tests/components/growth/GrowthAssetLibrary.test.ts` | DONE | GS-041 | +| README and SSRF-guarded website media imports | `src/utils/growth/importCandidates.ts`, `src/server/growth/assets.ts`, `src/server/growth/signals.ts`, `src/server/routes/growth.ts`, `src/components/growth/GrowthAssetLibrary.tsx`, `/api/growth/assets/import-candidates`, `/api/growth/assets/import` | DONE | GS-042 | +| Generated SVG cards (release, milestone, stats, quote, what's new) | `src/types/growth.ts`, `src/server/growth/cards.ts`, `src/server/growth/assets.ts`, `src/components/growth/GrowthAssetLibrary.tsx`, `/api/growth/assets/cards` | DONE | GS-043 | +| Browser canvas rasterization at 2x | `src/utils/growth/rasterize.ts`, `tests/utils/growth/rasterize.test.ts` | DONE | GS-044 | +| Image clipboard copy with download fallback | `src/components/growth/GrowthMediaActions.tsx`, `src/components/growth/calendar/GrowthQueue.tsx`, `src/components/growth/ContentItemDrawer.tsx` | DONE | GS-045 | +| Media gate: `ready`, `scheduled`, and `published` require account-valid media | `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/components/growth/ContentItemDrawer.tsx`, `tests/server/growthStore.test.ts`, `tests/server/growthRoutes.test.ts` | DONE | GS-020, GS-024, and GS-046 | +| Media attachment and removal UI with asset ownership validation | `src/components/growth/GrowthMediaPicker.tsx`, `src/components/growth/ContentItemDrawer.tsx`, `src/utils/growth/contentMedia.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts` | DONE | GS-046 | +| Phase 4 closure and phase 5 task authoring | `growth-studio-project/tasks/GS-050.md` through `GS-057.md`, `growth-studio-project/tasks/PROGRESS.md`, `growth-studio-project/docs/GS_DECISIONS.md` | DONE | GS-047 | +| Opportunity rules producing interventions and explicit backlog scanning | `src/utils/growth/opportunityRules.ts`, `src/server/growth/rules.ts`, `src/server/growth/signals.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `src/components/growth/GrowthInterventions.tsx`, `/api/growth/interventions/scan` | DONE | GS-050 and GS-051 | +| Attribution of published items to snapshot deltas at 48 hours and 7 days | `src/utils/growth/attribution.ts`, `src/server/growth/attribution.ts`, `src/server/snapshots.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `/api/growth/performance`, `/api/growth/performance/refresh` | DONE | GS-052 | +| Channel and pillar performance summaries | `src/utils/growth/performanceSummary.ts`, `src/server/growth/performance.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/types/growth.ts`, `src/api/growth.ts`, `/api/growth/performance/summary` | DONE | GS-053 | +| Repository and global weekly Growth Review with explicit account or repository measurement refresh | `src/utils/growth/weeklyReview.ts`, `src/server/growth/review.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `src/components/growth/GrowthReview.tsx`, `src/styles/growth/review.css`, `/api/growth/review`, `/api/growth/performance/refresh`, `/growth/review`, `/growth/r/:owner/:repo/review` | DONE | GS-054 and GS-065 | +| Evergreen recycling into deduplicated ideas | `src/utils/growth/evergreen.ts`, `src/server/growth/recycling.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `src/components/growth/GrowthInterventions.tsx`, `src/components/growth/ContentItemDrawer.tsx`, `/api/growth/interventions/:id/recycle` | DONE | GS-050 and GS-055 | +| Bounded plan re-weighting from performance with plan-only snapshots and UI notice | `src/utils/growth/performanceWeights.ts`, `src/server/growth/performance.ts`, `src/server/growth/planner.ts`, `src/types/growth.ts`, `src/api/growth.ts`, `src/components/growth/calendar/GrowthPlanManager.tsx` | DONE | GS-056 | +| Phase 5 closure and phase 6 task authoring | `growth-studio-project/tasks/GS-060.md` through `GS-067.md`, `growth-studio-project/tasks/PROGRESS.md`, `growth-studio-project/docs/GS_DECISIONS.md` | DONE | GS-057 | +| Unified calendar read model, month and week views, repository colours, URL-backed filters, local-time rescheduling, drawer editing, and account-wide ICS export | `src/utils/growth/unifiedCalendar.ts`, `src/server/growth/calendar.ts`, `src/components/growth/calendar/GrowthUnifiedCalendar.tsx`, `src/components/growth/calendar/`, `/api/growth/calendar`, `/growth/calendar` | DONE | GS-060 and GS-061 | +| Multi-repository generation and pillar deconfliction | `src/utils/growth/planDeconfliction.ts`, `src/server/growth/planner.ts`, `src/server/growth/store.ts`, `src/server/routes/growth.ts`, `src/api/growth.ts`, `src/components/growth/calendar/GrowthUnifiedCalendar.tsx`, `src/components/growth/calendar/GrowthMultiPlanManager.tsx`, `/api/growth/plans/generate-multiple` | DONE | GS-062 | +| Growth-wide settings persistence and account-scoped API | `src/utils/growth/settings.ts`, `src/server/growth/settings.ts`, `src/types/growth.ts`, `src/api/growth.ts`, `/api/growth/settings` | DONE | GS-063 | +| Growth settings UI and repository default inheritance | `src/components/growth/GrowthSettings.tsx`, `src/utils/growth/profileDefaults.ts`, `src/server/growth/store.ts`, `src/styles/growth/library.css`, `/growth/settings` | DONE | GS-064 | +| Global Home summaries and explicit Review measurement refresh | `src/components/growth/GrowthHome.tsx`, `src/components/growth/GrowthReview.tsx`, `/growth`, `/growth/review` | DONE | GS-065 | +| Release documentation, CHANGELOG and final screenshots | `README.md`, `CHANGELOG.md`, `docs/images/` | DONE | GS-066 | +| Final responsive, accessibility, security, migration, and release QA | Growth Studio routes, APIs, stores, tests, and documentation | DONE | GS-067 | diff --git a/growth-studio-project/docs/GS_PLAN.md b/growth-studio-project/docs/GS_PLAN.md new file mode 100644 index 0000000..8fe1f03 --- /dev/null +++ b/growth-studio-project/docs/GS_PLAN.md @@ -0,0 +1,362 @@ +# Growth Studio plan + +Authoritative architecture and phase plan for the Growth Studio project. +Read it in full before every task. Sections marked *settled* are decisions +recorded in `GS_DECISIONS.md`; do not reopen them inside a task. + +## 1. Goal + +Turn Growth Studio from a panel inside the Goals tab into the central growth +tool of GitDeck: + +1. A **dedicated shell** at `/growth`, opened from the main menu in a new + window, that hides everything unrelated (dashboard filters sidebar, tab + strip, footer) and has its own navigation. +2. A **repository-centric workspace**: the repository is the entry point; + goals ("missions"), interventions, calendar, library and review are panels + of that workspace. +3. **Interventions as an editorial plan**: interventions and content items are + first-class persisted entities with status and dates; the AI fills a + calendar built from content pillars and per-channel cadence, then drafts + each slot with mandatory media. +4. A **measure-and-learn loop**: published content is attributed to metric + deltas, a weekly Growth Review reports what worked, and the next plan is + re-weighted accordingly. +5. A **unified calendar** across every repository with a growth profile. + +## 2. Hard constraints + +- Everything outside Growth Studio keeps its routes, layout and behavior. The + only change to the main application chrome is replacing the `goals` tab with + the Growth Studio link (`target="_blank"`, `rel="noopener"`). +- `AGENTS.md` rules: English everywhere; pure logic in `src/utils` with + mirrored tests in `tests/utils`; server logic tests in `tests/server`; no + tests under `src`; forge API access only through server endpoints; no + unrelated refactors; TypeScript for new files. +- Both `src/i18n/en.ts` and `src/i18n/it.ts` receive every new key. +- Publishing is copy-paste only. No social credentials, no outbound posting. +- Media is mandatory: a content item cannot become `ready` without at least one + media attachment. +- Schema changes are additive and idempotent (`CREATE TABLE IF NOT EXISTS`, + `ALTER TABLE ... ADD COLUMN` guarded by a `PRAGMA table_info` check). Existing + `repository_goals` and `repository_content_sources` rows keep working. +- New native dependencies are not allowed. Image rasterization happens in the + browser (SVG drawn on a canvas), not on the server. +- Each task ends with `growth-studio-project/scripts/validate-gs-task.sh` + printing `VALIDATION OK`, one conventional commit (scope `growth`), no push, + no `Co-Authored-By` trailer. + +## 3. Settled decisions (see GS_DECISIONS.md) + +- D-001 Repository is the entry point; goals are a panel. +- D-002 Growth Studio opens in a separate window from the main menu. +- D-003 Copy-paste publishing with mandatory media; images are copied to the + clipboard or downloaded, never posted by the app. +- D-004 Unified multi-repository calendar is in scope. +- D-005 Client-side rasterization for generated image cards. +- D-006 The loop runs with pi by default; Telegram notifications reuse the + Emailchef runner variables. + +## 4. Current state (inventory, verified 2026-09-04) + +Files a task will most often touch or extend: + +| Area | Files | Verified responsibilities | +|---|---|---| +| Types | `src/types/goals.ts` | Defines the four metrics in `GOAL_METRIC_DEFINITIONS`; `RepositoryGoal`; `GoalSuggestion`; `GoalProposal`; `GoalProposalsData`; `GoalContentSource`; media suggestions; and all ten legacy proposal formats. The current generator emits only `x-thread`, `linkedin-post`, and `mastodon-post`. | +| Server store | `src/server/goalStore.ts` | Lazily creates `repository_goals` with columns `id`, `account_id`, `repository`, `metric`, `target_value`, `current_value`, `deadline`, `created_at`, `updated_at`, `suggestions`, and `suggestions_generated_at`, plus `repository_content_sources` with `account_id`, `repository`, `sources`, and `updated_at`. Suggestions and their nested proposals are JSON in `repository_goals.suggestions`. Exports account-scoped goal CRUD, current-value updates, suggestion/proposal saves, and content-source get/save helpers. | +| Server logic | `src/server/goals.ts` | `refreshGoal` uses metric resolvers for stars, forks, closed PRs, and release-asset downloads. `generateGoalSuggestions` uses a four-item `fallbackSuggestions` result when AI is not configured or structured generation yields an empty list, and otherwise requests 3–5 suggestions. `generateGoalProposals` requires AI and requests exactly one X thread, LinkedIn post, and Mastodon post. The module also privately fetches README and release signals, fetches repository and website source signals, guards website requests and redirects against SSRF, and exports `SOCIAL_PROPOSALS_VERSION` (currently `4`). | +| Goal routes | `src/server/routes/goals.ts` | Registers `GET /api/goals`, `POST /api/goals`, `DELETE /api/goals/:id`, `POST /api/goals/:id/advice`, and `POST /api/goals/:id/suggestions/:index/proposals`. Every handler requires the active account; create validates repository, metric, positive integer target, and date. | +| Content-source routes | `src/server/routes/repository.ts`, `src/server/routes/index.ts` | `registerRepositoryRoutes` registers account-scoped `GET` and `PUT /api/repository-content-sources?repo=owner/name`; the same route module also owns repository details, stargazers, forks, branches, and discussions. `registerApiRoutes` registers both repository and goal routes. | +| AI | `src/server/ai/client.ts`, `src/server/ai/settings.ts`, `src/server/aiDigest.ts` | `generateStructured` provides provider-specific structured JSON generation and throws `AiNotConfiguredError` or `AiRequestError`; `testAiConnection` performs the connection test. Settings resolve database overrides, environment variables, and defaults, expose `isAiConfigured`, and persist through `preferenceStore`. `maybeGenerateAiDigest` is a separate optional structured-output consumer. | +| Dashboard data | `src/server/dashboardData.ts` | Exports five-minute memoized `getReposCached`, `getIssuesCached`, and `getPullRequestsCached` loaders plus `invalidateDataCache`. Repository loads best-effort record and attach snapshots. | +| GitHub API helpers | `src/server/githubClient.ts` | Server-only authenticated GitHub GraphQL and REST helpers: `gql`, `restApi`, `restApiPaginate`, and the `ghApiJson` alias. | +| Historical data | `src/server/snapshots.ts`, `src/server/digests.ts` | Snapshots persist up to 90 daily star/fork records per repository in a JSON file, but `attachHistory` exposes only the latest 30. No raw snapshot reader is exported. Digests persist up to 120 daily records in a separate JSON file and expose daily/period delivery plus `getLatestRepoDigest`; AI enrichment is optional. | +| Persistence helpers | `src/server/sqlite.ts`, `src/server/preferenceStore.ts` | SQLite exports `getDatabase`, `execute`, `run`, `get`, `all`, and `closeDatabase`; the singleton database enables WAL and foreign keys. Preferences lazily create a global `preferences(scope, key, value, updated_at)` table and expose JSON `setPreference`, `getPreference`, and `deletePreference`. | +| SPA and entry point | `src/server/spa.ts`, `src/main.tsx` | The private `APP_ROUTES` set contains the fixed top-level routes including `/goals`; `isAppRoute` tests that set, while `isClientRoutePath` accepts any extensionless final path segment. `main.tsx` mounts only `App` inside shared `I18nProvider`, `AccountProvider`, and `BrowserRouter`. | +| Dashboard tab | `src/App.tsx` | Defines the `Tab` union and `TAB_ROUTES`, derives a tab from the pathname, owns goal loading state/effects, applies `tab-goals`, gives Goals the repository-filter search branch, renders the goals tab button, and mounts `GoalsView`. See section 4.2. | +| Goals UI | `src/components/views/GoalsView.tsx`, `src/components/views/GoalsLoadingState.tsx`, `src/components/modals/GoalProposalsModal.tsx` | `GoalsView` creates/deletes goals, groups them by repository, refreshes advice, opens source and proposal modals, and links to AI preferences. The loading component is a layout-matched skeleton. The proposal modal loads/caches/regenerates drafts, handles no-AI/error states, copies text, renders X posts and media, and closes on Escape. | +| Shared goals controls | `src/components/common/RepositoryPicker.tsx`, `src/components/common/RepositoryContentSources.tsx`, `src/components/common/ContentSourcePicker.tsx` | Searchable repository combobox; account-scoped source-library modal with queued auto-save; and repository/website source editor. `RepositoryPicker` currently contains the hard-coded English strings `repositories`, `No repositories found`, and `No description`; the other two use `goals.*` translations. | +| AI settings UI | `src/components/preferences/AiIntegrationSettings.tsx` | Loads, edits, tests, and resets the server-side AI provider settings and displays each resolved setting source. It is mounted by `PreferencesView` at `/preferences#preferences-ai`. | +| Client API | `src/api/github.ts` | Goal methods are `fetchGoals`, `createGoal`, `deleteGoal`, `generateGoalAdvice`, and `fetchGoalProposals`. Source methods are `fetchRepositoryContentSources` and `updateRepositoryContentSources`; AI settings methods are in the same module. | +| Pure goal utils | `src/utils/goals.ts`, `tests/utils/goals.test.ts` | Groups goals by repository, calculates bounded progress/deadline state, and formats an X thread for copying; mirrored tests cover all three. | +| Pure proposal utils | `src/utils/socialProposals.ts`, `tests/utils/socialProposals.test.ts` | Normalizes source entries, extracts web text/media URLs, attaches source media, counts/validates platform content, normalizes AI proposals, and checks the three-format social set; mirrored tests cover these behaviors. | +| AI tests | `tests/server/aiClient.test.ts`, `tests/server/aiSettings.test.ts` | Cover JSON parsing and provider wire formats/errors, plus settings precedence, reset, provider switching, and URL validation. | +| i18n | `src/i18n/en.ts`, `src/i18n/it.ts` | Both files contain the same 70-key Goals set listed in section 4.1. | +| Styles | `src/styles/goals.css`, `src/styles/layout-sidebar.css`, `src/styles/navigation.css`, `src/styles/tokens.css` | Goals CSS also owns repository-picker, source-picker, and proposals-modal styles. Layout/sidebar and navigation own dashboard chrome and tabs; tokens define dark, light, and automatic-theme values. All four are imported by `src/styles.css`. | +| Scripts | `package.json` | `dev` runs the TSX API watcher and Vite concurrently; `build` bundles the Node server with esbuild then runs Vite; `test` is `vitest run`; `typecheck` is `tsc --noEmit`. | + +### 4.1 Goals i18n inventory + +`src/i18n/en.ts` and `src/i18n/it.ts` have identical key sets: one +`tabs.goals` key and 69 `goals.*` keys. These are the keys that later shell and +Missions work must preserve or deliberately replace: + +```text +tabs.goals +goals.createTitle +goals.createDescription +goals.repository +goals.chooseRepository +goals.searchRepository +goals.metric +goals.target +goals.deadline +goals.add +goals.emptyTitle +goals.emptyText +goals.deleteConfirm +goals.deleteTitle +goals.deleteMessage +goals.completed +goals.remaining +goals.overdue +goals.daysLeft +goals.aiPlan +goals.mission +goals.completedMissions +goals.growthStudioEyebrow +goals.growthStudio +goals.growthStudioDescription +goals.generateAdvice +goals.refreshAdvice +goals.proposals +goals.proposalsOpen +goals.proposalsKind +goals.proposalsIntro +goals.sourcesTitle +goals.sourcesDescription +goals.sourcesRepository +goals.sourcesChooseRepository +goals.sourcesWebsite +goals.sourcesAdd +goals.sourcesRepoBadge +goals.sourcesWebBadge +goals.sourcesRemove +goals.sourcesInvalid +goals.sourcesLimit +goals.sourcesOptional +goals.mediaTitle +goals.mediaImage +goals.mediaVideo +goals.proposalsReadyTitle +goals.proposalsReadyText +goals.proposalsGenerate +goals.proposalsRetry +goals.proposalsLoading +goals.proposalsRegenerate +goals.proposalsRegenerateSources +goals.proposalsGeneratedAt +goals.proposalsNoAi +goals.proposalsOpenPreferences +goals.proposalsEmpty +goals.proposalFormat.x-thread +goals.proposalFormat.linkedin-post +goals.proposalFormat.mastodon-post +goals.proposalFormat.post +goals.proposalFormat.issue +goals.proposalFormat.discussion +goals.proposalFormat.email +goals.proposalFormat.checklist +goals.proposalFormat.message +goals.proposalFormat.doc +goals.copyThread +goals.copyPost +goals.aiFallback +``` + +Four of these keys are currently defined in both locales but have no source +consumer: `goals.chooseRepository`, `goals.deleteConfirm`, +`goals.sourcesRepository`, and `goals.proposalsRegenerateSources`. The proposal +format lookup is dynamic, so all `goals.proposalFormat.*` keys remain reachable +for legacy stored proposals even though new generation emits only three social +formats. + +### 4.2 `goals` tab consumers in `src/App.tsx` + +GS-012 must account for all of these direct consumers when it replaces the tab +with the external Growth Studio link: + +| Concern | Current consumer | +|---|---| +| Imports | `fetchGoals`, `GoalIcon`, `GoalsView`, and the `RepositoryGoal` type exist only for the Goals tab in `App.tsx`. | +| Tab type and route tables | `Tab` includes `goals`; `TAB_ROUTES.goals` is `/goals`; `ROUTE_TABS` derives the reverse mapping; `tabFromPath` therefore selects `goals` for `/goals`. | +| View selection | `tab` comes from `location.pathname`; `view` mirrors it outside Preferences, enabling the Goals render branch. | +| State | `goals` and `goalsLoaded` hold the list and initial-load state. | +| Account lifecycle | `handleBaseAccountChange` clears `goals` and resets `goalsLoaded`; `handleLogout` does the same. | +| Refresh callback | `refreshGoals` calls `fetchGoals`, replaces `goals`, and marks them loaded; it is passed to `GoalsView` as `onChange`. | +| Route-triggered loading effect | When authenticated and `tab === "goals"`, an abortable effect calls `fetchGoals`; success stores the list and all non-abort failures still mark the initial load complete. The effect reruns for auth state, active account, or tab changes. | +| Dashboard data dependency | `tab` is passed to `useDashboardData`; `dataRequirementsForTab` (in `src/utils/dataRequirements.ts`) currently maps `goals` to the repositories resource used by the picker. Top-bar refresh calls the same helper with `tab`. | +| Body class | The body-class effect toggles `tab-goals` when `tab === "goals"`. | +| Search and filters | Goals shares `repoFilters.search` with repositories, insights, alerts, and digests. `setSearch` updates `repoFilters` and resets `repoPage`; `resetFilters` restores `defaultRepoFilters`. `GoalsView` nevertheless receives the unfiltered `repos` array. | +| Generic tab propagation | `tab` is passed to `SidebarControls`; `navigateTab` and `TAB_ROUTES[tab]` are used by tab buttons, modal closing, and `CommandPalette` navigation. These generic paths continue to compile only if their tab types remain compatible after `goals` is removed. | +| Tab-strip entry | `tabs` adds the `goals` item with `tabs.goals`, `goals.length`, `goalsLoaded`, and `GoalIcon`; the shared map renders it as a ` ))} + + + {t("tabs.growthStudio")} + + ) : null} - {tab === "inbox" ? ( + {view === "inbox" ? ( ) : null} - {tab === "issues" ? ( + {view === "issues" ? (
{t("stats.openIssues")}
{countText(filteredIssues.length, issuesLoaded)}
{t("stats.matchingFilters")}
@@ -807,7 +833,7 @@ export function App() {
) : null} - {tab === "prs" ? ( + {view === "prs" ? (
{t("stats.openPrs")}
{countText(filteredPullRequests.length, prsLoaded)}
{t("stats.matchingFilters")}
@@ -851,7 +877,7 @@ export function App() {
) : null} - {tab === "repos" ? ( + {view === "repos" ? (
{t("stats.repositories")}
{countText(filteredRepos.length, reposLoaded)}
{t("stats.matchingFilters")}
@@ -892,7 +918,7 @@ export function App() {
) : null} - {tab === "insights" ? ( + {view === "insights" ? (
{t("stats.averageHealth")}
{countText(averageHealth, insightsLoaded)}
{t("stats.acrossTrackedRepos")}
@@ -904,7 +930,7 @@ export function App() {
) : null} - {tab === "alerts" ? ( + {view === "alerts" ? (
{t("alerts.totalAlerts")}
{countText(totalSecurityAlerts, insightsLoaded)}
{t("alerts.affectedRepos", { count: countText(securityRepoCount, insightsLoaded) })}
@@ -922,7 +948,7 @@ export function App() {
) : null} - {tab === "ci" ? ( + {view === "ci" ? ( (() => { const totalRuns = ciHealth.reduce((sum, entry) => sum + entry.totalRuns, 0); const totalFailures = ciHealth.reduce((sum, entry) => sum + entry.failureCount, 0); @@ -944,7 +970,7 @@ export function App() { })() ) : null} - {tab === "digests" ? ( + {view === "digests" ? (
{digestPeriod === "day" ? t("stats.digestDays") : digestPeriod === "week" ? t("stats.digestWeeks") : t("stats.digestMonths")}
{countText(dailyDigests.length, digestsLoaded)}
{digestPeriod === "day" ? t("stats.daysWithSavedSnapshots") : t("stats.periodsAggregated")}
@@ -957,7 +983,7 @@ export function App() {
) : null} - {tab === "kanban" && projectsEnabled ? { setBoardCount(count); setBoardLoaded(true); }} /> : null} + {view === "kanban" && projectsEnabled ? { setBoardCount(count); setBoardLoaded(true); }} /> : null}