Stage 0: governance scaffolding + monorepo bootstrap
Phase 1 foundation for the Stargue Publishing Engine (plan v2, BMAD
panel-reviewed 2026-04-19 — 1 APPROVE, 6 REVISE, 0 REJECT; all principles >=3).
- Governance doctrine adopted from DQMS
(.clinerules/12-foundational-principles.md,
.claude/hooks/gate-plan-exit.sh, .claude/skills/bmad-plan/SKILL.md)
- Bun workspaces + Turbo; apps/{mcp-linkedin,scheduler,admin};
packages/{schema,sanitize,linkedin-client,observability}
- Drizzle schema (content, publications, approvals, metrics,
linkedin_tokens, audit, outlet_feature_flags) with idempotency_key
UNIQUE and kill-switch table per TEA/dev panel revisions
- LinkedIn API canon: Posts API /rest/posts (not legacy UGC); OAuth
auth-code without PKCE; secretbox (not sealed-box); Community
Management API as separate approval gate from MDP
- Frontmatter Zod schema (status, language, outlets[], sanitize,
scheduled, version)
- Pino observability with PII redaction
- Expand-then-contract migration runbook
- Plan + panel verdicts mirrored to docs/plans/
- Deferred gates logged (Dokploy PaaS verification, LinkedIn Dev
Portal app registration)
bun install + bun run typecheck both exit 0 across 11 workspaces.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
docs/deferred-gates.md
Normal file
27
docs/deferred-gates.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Deferred gates
|
||||
|
||||
Items that could not be completed in the Stage 0 execution window and must be resolved before the gate they block.
|
||||
|
||||
## Gate 0.8 — Postgres + Redis on PaaS
|
||||
|
||||
**Status:** Deferred 2026-04-19.
|
||||
**Reason:** Dokploy MCP tools (`mcp__dokploy__*`) were not loaded in the Stage 0 session, and `DOKPLOY_API_KEY` was not in the WSL environment. Direct API query not possible in-session.
|
||||
**Blocks:** Stage 2 (database + API) — the scheduler and admin cannot run without a Postgres + Redis pair.
|
||||
**Resolution options:**
|
||||
1. **Recommended:** Restart Claude Code with the vault's `.mcp.json` Dokploy MCP attached; run `mcp__dokploy__list_applications` / equivalent against `sg-paas-s1.stargue.net` to verify or provision.
|
||||
2. Angelo confirms via Dokploy web UI and reports service names + connection hostnames.
|
||||
3. Provision fresh Postgres + Redis services in the `stargue-publishing-engine` Dokploy stack (clean room; isolates blast radius from other PaaS apps).
|
||||
|
||||
**Acceptance when resolved:** this file updated with service names, hostnames, and connection-string env-var locations (Dokploy secrets).
|
||||
|
||||
## Gate 0.9 — LinkedIn Developer Portal apps
|
||||
|
||||
**Status:** Pending Angelo's manual action.
|
||||
**Reason:** Dev Portal registration cannot be automated — requires human auth at `linkedin.com/developers/`.
|
||||
**Blocks:** Stage 3.1 (OAuth flow) live integration — structure + contract tests can proceed in parallel.
|
||||
**Resolution:** follow the checklist in [`linkedin-apps.md`](./linkedin-apps.md). Expected time ~30 min for OIDC + Share; multi-week for Community Management API + MDP partner approvals.
|
||||
**Acceptance when resolved:** `linkedin-apps.md` registry filled in with app IDs; client credentials stored in Dokploy secrets `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, `LINKEDIN_TOKEN_ENCRYPTION_KEY`.
|
||||
|
||||
## What proceeds in parallel
|
||||
|
||||
Stages 1–2 (local DB), 3 structural (OAuth code + tool shapes + msw fixtures), 4 (scheduler), 5 (admin UI with Authentik stub), 6 (sanitize integration in stargue-com/.net), 7.1 (`/market audit` baseline), 7.2 (7 Cs post drafting) can all proceed without these gates. Live integration tests and first production publish require both gates resolved.
|
||||
46
docs/linkedin-apps.md
Normal file
46
docs/linkedin-apps.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# LinkedIn Developer Portal — Application Registry
|
||||
|
||||
**Status tracker for Stargue's LinkedIn apps.** Every row must stay current — Claude Code verifies against this file during Stage 0 DoD 0.9.
|
||||
|
||||
## Applications
|
||||
|
||||
| App | Product | Purpose | Scopes granted | State | App ID | Approval date | Notes |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Stargue Publishing Engine — Sign In | Sign In with LinkedIn using OpenID Connect | Identity / `whoami` | `openid profile email` | **TODO: register** | — | — | Standard — no approval required beyond Dev Portal self-service |
|
||||
| Stargue Publishing Engine — Member Share | Share on LinkedIn | Personal-profile posts (Angelo) | `w_member_social` | **TODO: register** | — | — | Standard — attached to the same app as Sign In |
|
||||
| Stargue Publishing Engine — Org Share | Community Management API | Company-page posts (`urn:li:organization:2605890`) | `w_organization_social`, `r_organization_social` | **TODO: apply** | — | — | Requires LinkedIn partner approval; submit via Dev Portal "Request access" |
|
||||
| Stargue Publishing Engine — Refresh | Marketing Developer Platform (MDP) | Programmatic refresh tokens (eliminates 60-day re-auth) | (extends above) | **TODO: apply** | — | — | Requires LinkedIn partner approval; multi-week review |
|
||||
|
||||
## OAuth redirect URIs
|
||||
|
||||
- Production: `https://publishing.stargue.net/auth/linkedin/callback`
|
||||
- Local dev: `http://localhost:3002/auth/linkedin/callback`
|
||||
|
||||
## Client credentials
|
||||
|
||||
Stored in Dokploy secrets on `sg-paas-s1.stargue.net`:
|
||||
- `LINKEDIN_CLIENT_ID`
|
||||
- `LINKEDIN_CLIENT_SECRET`
|
||||
- `LINKEDIN_TOKEN_ENCRYPTION_KEY` (32-byte key for `crypto_secretbox`)
|
||||
|
||||
## Manual setup checklist (Stage 0.9)
|
||||
|
||||
1. Sign in to `https://www.linkedin.com/developers/` as Angelo (personal account).
|
||||
2. Create app "Stargue Publishing Engine" linked to Stargue Company Page `urn:li:organization:2605890` (required for Community Management API eligibility).
|
||||
3. Under **Products**, request:
|
||||
- Sign In with LinkedIn using OpenID Connect (self-service; instant)
|
||||
- Share on LinkedIn (self-service; instant)
|
||||
- Community Management API (submit partner-access request — fill in use-case: "Programmatic posting of original content to Stargue Company Page for knowledge-management-as-a-service practice")
|
||||
- Marketing Developer Platform (separate partner-access request — fill in use-case: "Automate posting and engagement-metric collection for owned content from a self-hosted publishing pipeline")
|
||||
4. Under **Auth**, add redirect URIs above.
|
||||
5. Copy Client ID and Client Secret to Dokploy secrets (never commit).
|
||||
6. Update this file with the app ID.
|
||||
7. Re-request Community Management API + MDP if denied initially, with more detailed use-case.
|
||||
|
||||
## References
|
||||
|
||||
- [LinkedIn Developer Portal](https://www.linkedin.com/developers/)
|
||||
- [Posts API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api)
|
||||
- [3-Legged OAuth Flow](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow)
|
||||
- [Programmatic Refresh Tokens](https://learn.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens)
|
||||
- [Community Management API overview](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/)
|
||||
397
docs/plans/2026-04-19-phase1-plan.md
Executable file
397
docs/plans/2026-04-19-phase1-plan.md
Executable file
@@ -0,0 +1,397 @@
|
||||
---
|
||||
created: 2026-04-19T09:45
|
||||
updated: 2026-04-19T10:45
|
||||
tags:
|
||||
---
|
||||
|
||||
# Plan — Phase 1 Automated Publishing Engine (with LinkedIn MCP Server)
|
||||
|
||||
**Revision:** v2 (consolidated from 7-agent BMAD panel review 2026-04-19)
|
||||
**Verdicts file:** `/tmp/bmad-panel-verdicts-publishing-engine-phase1-20260419.json` (WSL)
|
||||
**Panel result:** 1 APPROVE, 6 REVISE, 0 REJECT — all principles ≥3 (gate passes). Revisions below address each REVISE gap, citing originating agent.
|
||||
|
||||
**Applicability:** tier-2 = [RBR, A11y, Testability]
|
||||
**Precedence:** `CLAUDE.md > Conflict Resolution Precedence` + adopted doctrine at `/home/devuser/projects/dqms/.clinerules/12-foundational-principles.md` (user directive 2026-04-19)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context
|
||||
|
||||
User directive 2026-04-19: **Full Phase 1 automation, now.** No manual posting, no postponing. LinkedIn programmatic refresh is gated to MDP partners per [Microsoft Learn > Programmatic Refresh Tokens (retrieved 2026-04-19)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens). Non-MDP apps must re-auth every ≤60 days; we accept this as the honest fallback and file MDP in parallel.
|
||||
|
||||
Existing state (verified 2026-04-19):
|
||||
- `stargue-com` and `stargue-net` both on Next.js 16 + React 19 + Bun + Tailwind v4 + remark (verified against `package.json` per **dev** agent). Note: **stargue-net/package.json is a copy of stargue-com** (same `name` field) — **name collision to resolve before workspace linking** (dev gap 5).
|
||||
- Listmonk email capture + CTA + content protection already shipped on stargue.com.
|
||||
- Dokploy on `sg-paas-s1.stargue.net`; Gitea at `git.stargue.net`; OpenObserve observability stack.
|
||||
- Stargue LinkedIn Company Page: `urn:li:organization:2605890` (user-confirmed 2026-04-19).
|
||||
- Authentik SSO: user-confirmed running; all Stargue services share the same instance.
|
||||
|
||||
**Capacity reconciliation (PM/SM gap).** PRD §6.5 caps Angelo at 4h/week. The user has explicitly overridden this cap for this Phase 1 build — "no postponing, fully functional now." Plan is re-expressed below as **concentrated full-time build (14 calendar days)** with the user accepting timeline risk explicitly. If the user later reverts to the 4h/week cap, Stages 2–7 re-expand to ~15 weeks at 4h/week.
|
||||
|
||||
---
|
||||
|
||||
## 2. Objectives
|
||||
|
||||
1. **Automated two-channel publish pipeline** — Obsidian vault → stargue.com/.net (git push → Dokploy) + LinkedIn (personal profile `angeloluidens` + company page `urn:li:organization:2605890`) via a **Stargue LinkedIn MCP Server**.
|
||||
2. **Optimized cadence** — bootstrap Tue + Thu 08:30 AM AST; adaptive refinement after data accrues (≥8 samples per (day,hour) slot), proposed-not-auto-applied.
|
||||
3. **Fail-closed content sanitization** — wikilink/Obsidian strip + private-path blocklist + tag firewall + per-outlet length validation.
|
||||
4. **Admin dashboard** — approval queue, side-by-side outlet preview, per-outlet status, scheduled-publish controls, metrics heatmap.
|
||||
5. **PostgreSQL content registry** (PaaS) — SSoT for publication state, idempotency, audit; nightly pg_dump → Neon free tier for DR.
|
||||
6. **OAuth 2.0 handled honestly** — 60-day re-auth fallback with proactive T-5-day notification + fail-safe halt; MDP app filed in parallel.
|
||||
7. **Revenue conversion preserved** (PM gap). Phase 1 must not regress the already-shipped revenue surfaces on stargue.com (Listmonk email capture, CTAs, service page, Terms of Use). New inbound-inquiry tracking column on `publications.metadata_jsonb.inquiries` (counter incremented when a contact-page submission cites the post's slug as referrer).
|
||||
8. **Kill-switch** — per-outlet feature flag (`OUTLET_ENABLED:linkedin=false`) readable at runtime without redeploy, toggled via admin dashboard. TEA gap 10.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 Repository topology (simplified per **architect** gap 1)
|
||||
|
||||
Collapsed `apps/api` into `apps/admin` Next.js App Router route handlers. Scheduler imports `linkedin-client` directly as a library; MCP transport is **stdio only** (for Claude Code use); HTTP transport deferred until a real cross-service need emerges (architect gap 2).
|
||||
|
||||
```
|
||||
stargue-publishing-engine/
|
||||
├── apps/
|
||||
│ ├── mcp-linkedin/ MCP server (stdio transport, TypeScript/Bun)
|
||||
│ ├── scheduler/ BullMQ worker; imports packages/linkedin-client directly
|
||||
│ └── admin/ Next.js App Router (UI + /api/* route handlers)
|
||||
├── packages/
|
||||
│ ├── sanitize/ remark plugin + blocklist + length validation + test corpus
|
||||
│ ├── schema/ Zod schemas, Drizzle schema, frontmatter parser (SSoT)
|
||||
│ ├── linkedin-client/ LinkedIn Posts API client + encrypted token store
|
||||
│ └── observability/ Pino + OTel + correlation IDs
|
||||
├── infra/
|
||||
│ ├── docker-compose.yml Local dev (postgres + redis + openobserve)
|
||||
│ └── dokploy/ Service definitions
|
||||
├── db/migrations/ Drizzle expand-then-contract migrations
|
||||
├── .clinerules/ Adopted from dqms
|
||||
├── .claude/
|
||||
│ ├── hooks/gate-plan-exit.sh
|
||||
│ └── skills/bmad-plan/SKILL.md
|
||||
├── test/corpus/private/ 12-file private-vault fixture (sanitizer test SSoT) — SM gap 2
|
||||
├── docs/plans/ Filed plans
|
||||
├── CLAUDE.md
|
||||
├── package.json Bun workspaces
|
||||
└── turbo.json
|
||||
```
|
||||
|
||||
### 3.2 LinkedIn API — corrected per **dev** agent
|
||||
|
||||
**Endpoint (dev gap 3):** Posts API `https://api.linkedin.com/rest/posts` — NOT legacy `/v2/ugcPosts`. Required headers: `LinkedIn-Version: 202404` (or current YYYYMM per release cadence), `X-Restli-Protocol-Version: 2.0.0`, `Authorization: Bearer <access_token>`. Request shape uses flat `content` + `distribution` (not nested `specificContent/com.linkedin.ugc.ShareContent`).
|
||||
|
||||
**Auth flow (dev gap 2):** OAuth 2.0 Authorization Code flow — **no PKCE** (LinkedIn 3LO does not document PKCE support per [Microsoft Learn > 3-Legged OAuth Flow](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow), retrieved 2026-04-19). We use confidential-client with `client_secret` + CSRF-protected `state` parameter.
|
||||
|
||||
**Products required in LinkedIn Dev Portal (dev gap 1):**
|
||||
| Product | Scopes granted | Purpose | Approval gate |
|
||||
|---|---|---|---|
|
||||
| **Sign In with LinkedIn using OpenID Connect** | `openid profile email` | Identity | Standard |
|
||||
| **Share on LinkedIn** | `w_member_social` | Personal-profile posts | Standard |
|
||||
| **Community Management API** | `w_organization_social`, `r_organization_social` | Company-page posts | **Requires LinkedIn partner approval — separate from MDP, non-trivial gate** |
|
||||
| **Marketing Developer Platform (MDP)** — aspirational | Programmatic refresh tokens | Eliminate 60-day re-auth | Multi-week review |
|
||||
|
||||
**Impact:** Company Page posting (Objective 1, personal + `org:2605890`) may be blocked pending Community Management API approval. Personal-profile posting unblocked immediately. We file Community Management API application in Stage 0.5 and build the tools so they're ready when approved.
|
||||
|
||||
**Token store (architect gap 3):** Encryption primitive corrected to **libsodium `crypto_secretbox`** (symmetric XSalsa20-Poly1305) with a 32-byte key stored in Dokploy secrets. Sealed-box was wrong for at-rest DB encryption. Alternative considered: KMS envelope encryption via age-based encryption — deferred (no KMS in Stargue PaaS yet; secretbox+secret is adequate for Phase 1 blast radius).
|
||||
|
||||
**Refresh concurrency (TEA gap 3):** Token rotation path acquires a Postgres advisory lock keyed on `hashtextextended(subject_urn, 0)` before reading/refreshing. Lock released after commit. Prevents double-rotation race.
|
||||
|
||||
**Idempotency (dev gap 4, TEA gap 5):** `publications` table has `idempotency_key TEXT UNIQUE NOT NULL` populated as `sha256(content_id || outlet || scheduled_at)`. LinkedIn handler refuses to dispatch if a `publications` row with `external_id IS NOT NULL` already exists for the key. BullMQ jobs also carry an `idempotency_key` to dedupe retries before they reach the handler.
|
||||
|
||||
**Rate limits (architect gap 4).** LinkedIn does not publicly document a fixed per-member posting cap. We impose **self-imposed conservative limits** (not LinkedIn quota): 20 posts/day/member, 50 posts/day/org. Stage 3 DoD asserts this via a test (TEA gap 6).
|
||||
|
||||
**MCP tools exposed (stdio transport, no HTTP):**
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `linkedin_whoami` | Returns authenticated subject + scopes + MDP-eligible flag |
|
||||
| `linkedin_auth_status` | Access-expiry remaining, refresh-expiry remaining, kill-switch state |
|
||||
| `linkedin_create_post` | Text post via Posts API (`author_urn` param: `urn:li:person:<id>` or `urn:li:organization:2605890`) |
|
||||
| `linkedin_create_article` | Long-form article (≤125k chars) |
|
||||
| `linkedin_upload_media` | Image / PDF carousel; returns asset URN |
|
||||
| `linkedin_create_post_with_media` | Combines upload + post |
|
||||
| `linkedin_delete_post` | Delete post by URN (soft-reversible within 5-min edit window only; after that, delete-only) |
|
||||
| `linkedin_get_post_metrics` | Impressions/reactions/comments/shares/clicks for a URN |
|
||||
| `linkedin_get_profile_stats` | Aggregate profile stats for cadence optimizer |
|
||||
|
||||
### 3.3 Sanitization pipeline
|
||||
|
||||
`packages/sanitize` — Unified/remark plugin chain:
|
||||
1. Strip wikilinks `[[Path/Note|Display]]` → `Display` or plain removal.
|
||||
2. Strip embeds `![[file]]` → resolve (blog) or remove (LinkedIn).
|
||||
3. Strip dataview blocks.
|
||||
4. Strip callouts.
|
||||
5. **Private-path blocklist** — `Family Matters/`, `Financial Matters/`, `Journal/`, `Clients/*[NDA]/`, extensible. Match triggers **build error**, not silent strip.
|
||||
6. **Tag firewall** — `#private`, `#heal-internal`, `#confidential` trigger build error.
|
||||
7. Per-outlet length validation (LinkedIn 3k / article 125k / Twitter 280 / etc.).
|
||||
8. Idempotency hash output (`content_hash`) over sanitized body for dedupe.
|
||||
|
||||
**Test corpus (SM gap 2, TEA gap 8):** `test/corpus/private/*` contains 12 fixture files — 4 blocklist-path, 4 tag-firewall, 4 length-overflow. Stage 1 DoD: **"CI passes when all 12 round-trip with build errors AND all reference clean files round-trip without errors."** Promoted from Risks to Stage 1 DoD.
|
||||
|
||||
### 3.4 Content registry (Postgres) + DR
|
||||
|
||||
Drizzle schema in `packages/schema/db.ts`:
|
||||
|
||||
- `content(id, vault_path, slug, title, body_sanitized, frontmatter_jsonb, content_hash, version, created_at, updated_at)`
|
||||
- `publications(id, content_id, outlet, status, scheduled_at, published_at, external_id, external_url, idempotency_key UNIQUE, error, metadata_jsonb)`
|
||||
- `approvals(id, content_id, outlet, approved_by, approved_at, notes)`
|
||||
- `metrics(id, publication_id, collected_at, impressions, reactions, comments, shares, clicks, raw_jsonb)`
|
||||
- `linkedin_tokens(id, subject_type, subject_urn, access_token_ct, refresh_token_ct, access_expires_at, refresh_expires_at, scopes, created_at, updated_at)`
|
||||
- `audit(id, ts, actor, action, subject_type, subject_id, correlation_id, payload_jsonb)` — append-only
|
||||
- `outlet_feature_flags(outlet, enabled, reason, updated_at, updated_by)` — kill-switch (Objective 8)
|
||||
|
||||
**Migrations (architect gap 7, TEA gap 4):** Every schema change follows **expand-then-contract**. Example for idempotency_key rollout: (1) add column nullable, (2) backfill via one-time script with SELECT … UPDATE … WHERE, (3) `NOT NULL` + `UNIQUE` constraint added in a second migration, (4) contract legacy column in a third migration once no code writes it. Each migration accompanied by a runbook in `db/migrations/README.md`. **Stage 2 DoD:** "Migration rehearsal on staging DB captures full expand→backfill→contract sequence with rollback point documented."
|
||||
|
||||
**DR mirror:** nightly cron runs `pg_dump --format=c` streamed to Neon free tier via Neon connection string in Dokploy secrets. Retains only last 30 days of metrics on the mirror (respects Neon 0.5 GB free cap). RPO ~24h.
|
||||
|
||||
### 3.5 Admin dashboard + A11y (UX gaps 1–8)
|
||||
|
||||
Next.js App Router. Authentik SSO (user-confirmed instance). Routes also host `/api/*` handlers (architect gap 1 collapse).
|
||||
|
||||
**Pages:**
|
||||
- `/queue` — scheduled + pending items, side-by-side outlet preview, approval CTA
|
||||
- `/publications/:id` — per-outlet status, metrics, errors
|
||||
- `/auth/linkedin` — OAuth flow + re-auth link + kill-switch toggle
|
||||
- `/metrics` — heatmap (day × hour × outlet)
|
||||
|
||||
**Nine Laws temperature declaration (UX gap 5):** Admin dashboard is an **operator tool**, not a reader surface. It inhabits the **cool / stargue.net (forge) temperature**. Forest-green (#4E8211) + Citrus (#99CC00) accents. **Amber (#F59E0B-family) reserved for Hearth content only — never leaks into admin status badges** (Law 4). Status badges use Forest/slate/red variants.
|
||||
|
||||
**A11y DoDs (UX gaps 1–4, 6–8; dev gap A11y; TEA gap 7):**
|
||||
- Axe-core ruleset pinned to `axe-core@4.x`, severity gate: **zero `serious` or `critical` violations** on every route (SM gap 5).
|
||||
- WCAG 2.2 AA contrast verification via script that reads design-token file, asserts each FG/BG pair ≥4.5:1 body and ≥3:1 large-text/non-text-UI on **both themes**.
|
||||
- Screen-reader smoke test in CI: `@axe-core/playwright` + NVDA scripting via GitHub Actions on Windows runner for the approval flow; manual VoiceOver pass in release checklist.
|
||||
- `aria-live="polite"` on queue status transitions; `aria-live="assertive"` on publish errors only. Announcement text specified per surface.
|
||||
- Error association: every form input has `aria-describedby` → error-message container; WCAG 3.3.1/3.3.3/4.1.3 complied.
|
||||
- Focus trap on modal approval confirmations; skip-link on dashboard; `prefers-reduced-motion` honored for section fade-in (Law 8.4).
|
||||
- `/metrics` heatmap: **color + text redundancy** — each cell shows `HH:MM — N events` text on hover and in the cell body (not color-only). WCAG 1.4.1 satisfied.
|
||||
|
||||
### 3.6 Cadence optimizer (Analyst gap 3)
|
||||
|
||||
**Bootstrap:** Tue + Thu, 08:30 AM AST, user-confirmed.
|
||||
|
||||
**Adaptive (spec'd precisely):**
|
||||
- Model: Beta posterior over engagement rate (reactions+comments+shares ÷ impressions) per (day_of_week, hour_of_day) bucket.
|
||||
- Prior: `Beta(1, 19)` — weakly informative, centered on 5% baseline engagement to avoid spurious early moves.
|
||||
- Sample threshold: **≥8 posts per bucket AND ≥4 weeks of data** before the optimizer proposes a change.
|
||||
- Change trigger: posterior mean of an alternate bucket exceeds current bucket's posterior mean by ≥20% AND the 95% credible intervals do not overlap.
|
||||
- Output: **proposal in `/metrics` UI with "Approve", "Reject", "Snooze 4 weeks"** — never auto-applied. Human-in-the-loop (RBR).
|
||||
- Seams: optimizer is a pure function `propose(history, config) → [Proposal]` in `packages/schema`; clock/RNG injected; unit-tested with synthetic history.
|
||||
|
||||
### 3.7 Deployment + kill-switch
|
||||
|
||||
Services on Dokploy stack `stargue-publishing-engine`:
|
||||
- `admin` — Next.js standalone, port 3002, Traefik `publishing.stargue.net`, Authentik SSO
|
||||
- `scheduler` — BullMQ worker, no ingress
|
||||
- `mcp-linkedin` — MCP stdio, container runs only when Claude Code connects via `docker exec`
|
||||
- `postgres` — **confirm via Dokploy MCP at Stage 0 DoD** (architect gap 5). If no existing cluster, provision a new stack service.
|
||||
- `redis` — same confirmation path
|
||||
|
||||
**Kill-switch (Objective 8, TEA gap 10):** `outlet_feature_flags` table read by scheduler + MCP server on every dispatch. Toggle via admin dashboard at `/admin/feature-flags`. Flip takes effect within 30s without redeploy. Audit-logged.
|
||||
|
||||
**CSRF (architect gap 8):** Admin `/api/*` POST/DELETE handlers use double-submit cookie pattern via `@edge-csrf/nextjs` or equivalent. Authentik session cookie anchors identity; CSRF token anchors same-origin.
|
||||
|
||||
---
|
||||
|
||||
## 4. Stages + measurable DoD (all "works" language tightened per SM/TEA gaps)
|
||||
|
||||
### Stage 0 — Governance + gate resolution (day 1)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 0.1 Create Gitea repo | `GET https://git.stargue.net/api/v1/repos/admin/stargue-publishing-engine` returns 200 with `empty=false` |
|
||||
| 0.2 Install `.clinerules/` from dqms | `diff -q .clinerules/12-foundational-principles.md /home/devuser/projects/dqms/.clinerules/12-foundational-principles.md` reports no difference |
|
||||
| 0.3 Install `.claude/hooks/gate-plan-exit.sh` | Hook file present with exec perms; `bash -n .claude/hooks/gate-plan-exit.sh` parses clean |
|
||||
| 0.4 Install `.claude/skills/bmad-plan/SKILL.md` | File SHA256 matches dqms SKILL.md |
|
||||
| 0.5 Project CLAUDE.md | File exists and names: vault-as-SSoT, precedence, commands, outlet URNs |
|
||||
| 0.6 Bun workspaces + Turbo bootstrap | `bun install` exit 0; `bun run typecheck` exit 0 in empty state |
|
||||
| **0.7 Gates answered** (SM gap 7) | §11 Open Questions each have "Answered: <value, date>" row in `docs/plans/2026-04-19-phase1-plan.md` |
|
||||
| 0.8 Verify Postgres + Redis cluster on PaaS | Dokploy MCP query returns running service names OR new services provisioned and listed |
|
||||
| 0.9 File LinkedIn Dev Portal products | Apps registered: "Sign In + OIDC", "Share on LinkedIn", "Community Management API" (pending approval), "MDP" (pending approval). Screenshot URLs recorded in `docs/linkedin-apps.md` |
|
||||
| 0.10 Resolve stargue-net/package.json name collision (dev gap 5) | `stargue-net/package.json` has `"name": "stargue-net"` (distinct from stargue-com) |
|
||||
|
||||
### Stage 1 — Shared packages (days 2–3)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 1.1 `packages/schema` | 100% line coverage via Vitest; Zod round-trip test for frontmatter with 6 valid + 6 invalid cases |
|
||||
| 1.2 `packages/sanitize` | **CI passes when 12 `test/corpus/private/*` fixtures round-trip with build errors AND 6 clean-corpus fixtures round-trip without errors** (SM gap 2) |
|
||||
| 1.3 `packages/observability` | Pino JSON output includes `correlation_id`, `subject`, `level`, `msg`; validated via JSON-schema check on 100 sample logs |
|
||||
| 1.4 `packages/linkedin-client` | Fake client passes contract-test suite using [`msw`](https://mswjs.io/) (chosen recorder, dev gap 7) against recorded Posts API fixtures |
|
||||
|
||||
### Stage 2 — Database + core API (days 3–4)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 2.1 Drizzle expand-only migrations | `drizzle-kit migrate` applies all tables on empty DB; each migration has a rollback query recorded in `db/migrations/README.md` |
|
||||
| 2.2 **Migration rehearsal** (TEA gap 4) | Idempotency_key rollout rehearsed end-to-end on staging: expand→backfill→constraint→contract; runbook committed |
|
||||
| 2.3 Admin `/api/content`, `/api/approvals`, `/api/publications` handlers | Each route: Zod parse → Authentik session → Cerbos-equivalent authz check → DB write with correlation ID. Vitest integration tests assert 401/403/200/422 for each route |
|
||||
| 2.4 Rate limit on `/api/content` POST | `express-rate-limit`-equivalent middleware; burst test asserts HTTP 429 on request #21/min (TEA gap 6) |
|
||||
| 2.5 CSRF middleware | Double-submit cookie test: cross-origin POST returns 403 |
|
||||
|
||||
### Stage 3 — LinkedIn MCP server (days 4–7)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 3.1 OAuth auth-code flow (no PKCE, CSRF-state, personal) | Manual round-trip: auth URL → callback → token row persisted encrypted → `linkedin_whoami` returns subject |
|
||||
| 3.2 OAuth for Company Page (pending Community Management API approval) | Same flow w/ `w_organization_social`; if API approval pending, test skipped with recorded pending-state row in `docs/linkedin-apps.md` |
|
||||
| 3.3 Tools `linkedin_whoami`, `linkedin_auth_status` | Contract tests assert exact response shape against msw-recorded fixtures |
|
||||
| 3.4 Tool `linkedin_create_post` (personal) | Live integration test against **a throwaway test post**. DoD: "Post URN persisted to `publications.external_id`; status=published; **test post deleted within 5-min edit window; deletion audited in `audit` table**" (SM gap 3) |
|
||||
| 3.5 Tool `linkedin_upload_media` + `linkedin_create_post_with_media` | Image upload returns asset URN; PDF carousel post persisted; test posts deleted as 3.4 |
|
||||
| 3.6 Tool `linkedin_create_article` (personal) | Article URN persisted; test article deleted |
|
||||
| 3.7 Tool `linkedin_get_post_metrics` + `linkedin_get_profile_stats` | Metrics row written to `metrics` table; contract-tested |
|
||||
| 3.8 Refresh path with advisory lock + concurrent-access test | **Parallel test fires 4 concurrent rotate attempts on an expired token; exactly 1 rotation succeeds, 3 retry on the new token** (TEA gap 3) |
|
||||
| 3.9 Fail-safe halt on auth expiry | Forced-expiry test: `outlet_feature_flags.linkedin.enabled` auto-flips false; Telegram webhook receives notification; scheduler halts dispatch |
|
||||
| 3.10 Kill-switch toggle via admin | Manual test: flip via UI → flag updated within 30s → next dispatch attempt refuses with audit row (TEA gap 10) |
|
||||
| 3.11 Rate-limit self-cap | Burst test: 21 posts/min from same subject → 21st rejected with internal 429 + audit row |
|
||||
| 3.12 MCP stdio transport wiring | Claude Code `docker exec mcp-linkedin node dist/server.js` responds to `initialize` + `tools/list` per MCP spec `2024-11-05` (pinned) |
|
||||
|
||||
### Stage 4 — Scheduler (day 7–8)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 4.1 BullMQ queue + Redis | `bun test` integration asserts: enqueue → worker-consume → DB row updated within 5s |
|
||||
| 4.2 End-to-end publish loop | **Trace test: POST /api/content → approval → scheduled_at reached → git commit SHA recorded + LinkedIn URN recorded in `publications`, status=published, within 120s** (SM gap 4) |
|
||||
| 4.3 DLQ + retry + idempotency | Chaos test: kill LinkedIn fake mid-publish → retry with same `idempotency_key` → no duplicate post (TEA gap 5) |
|
||||
| 4.4 Cadence optimizer integration | Feature flag default OFF; unit tests assert proposals with ≥8 samples + ≥4 weeks threshold; no proposal below threshold |
|
||||
|
||||
### Stage 5 — Admin dashboard (days 8–10)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 5.1 Scaffold + Nine Laws tokens | Design tokens file matches Nine Laws doc via script diff; Lighthouse Performance ≥90, Accessibility ≥95 on `/queue` |
|
||||
| 5.2 All routes authz-gated by Authentik | Playwright test: unauthenticated request → 302 to login; wrong role → 403 |
|
||||
| 5.3 A11y gate | **Zero `serious` or `critical` axe-core violations on every route; contrast-check script exits 0 on both themes; @axe-core/playwright SR smoke passes the approval flow** (UX gaps 2, 3) |
|
||||
| 5.4 Manual A11y checklist | Keyboard-only walkthrough passes; VoiceOver pass on approval flow recorded in `docs/a11y-checklist.md`; focus-trap on modals verified |
|
||||
| 5.5 Heatmap pattern+text redundancy | Axe check + manual verification: heatmap cells render text + color, not color-only (UX gap 6) |
|
||||
|
||||
### Stage 6 — stargue-com/.net integration (day 10)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 6.1 Add `packages/sanitize` + `packages/schema` as workspace deps | `stargue-com` and `stargue-net` `bun run build` both succeed; rendered HTML contains zero wikilinks (assertion: `grep -c '\[\[' dist/**/*.html` = 0) |
|
||||
| 6.2 Replace ad-hoc frontmatter parsing | Diff: no occurrences of ad-hoc parser; all frontmatter flows through `packages/schema` parser |
|
||||
|
||||
### Stage 7 — Content + metrics bootstrap (days 11–14)
|
||||
|
||||
| Step | Measurable DoD |
|
||||
|---|---|
|
||||
| 7.1 `/market audit` baseline | Baseline score captured in `docs/marketing-baseline-2026-04.json` |
|
||||
| 7.2 Draft 7 Cs posts 2–8 (long + LinkedIn variants) | **14 files committed in `stargue-com/src/content/blog/7cs/` with schema-valid frontmatter; each passes autoresearch optimization loop with composite ≥8** |
|
||||
| 7.3 File MDP + Community Management API apps | Application IDs recorded in `docs/linkedin-apps.md` |
|
||||
| 7.4 First scheduled post lands on stargue.com + LinkedIn | **`publication.external_url` returns HTTP 200 from unauthenticated curl; stargue.com blog route returns HTTP 200; metrics row collected at T+48h±1h** (SM gap 6) |
|
||||
| 7.5 DR test | pg_dump to Neon succeeds and restores to verifiable row count |
|
||||
|
||||
---
|
||||
|
||||
## 5. Principle adherence — revised post-panel
|
||||
|
||||
| Principle | v1 target | v2 target | Change rationale |
|
||||
|---|---|---|---|
|
||||
| SEBP | 4 | 5 | Simpler topology (apps/api collapsed into admin), clearer boundaries; composition over separation. |
|
||||
| SSoT | 5 | 5 | Unchanged (panel consensus). Name-collision fix in 0.10. |
|
||||
| FPT | 4 | 5 | **Fixed:** PKCE dropped, Posts API (not UGC), secretbox (not sealed-box), Community Management API gate named, stargue-net naming, rate-limit framed as self-imposed, msw as contract-test recorder. All per dev + architect verification. |
|
||||
| DiDSP | 5 | 5 | Idempotency_key + advisory lock + CSRF + kill-switch added. |
|
||||
| PbD | 4 | 5 | Added DPIA note (§9); token retention explicit; SAR path DoD-tested via erasure integration test in Stage 2. |
|
||||
| OF | 5 | 5 | Unchanged (panel consensus 5 on OF). |
|
||||
| RBR | 4 | 5 | Kill-switch + migration rehearsal DoD + 5-min-edit-window rollback acknowledgement + test-post deletion in Stage 3 DoDs. |
|
||||
| A11y | 4 | 5 | SR protocol, aria-live levels specified, error association, dual-theme contrast script, heatmap pattern+text, focus-trap, temperature declared, operator persona question added. |
|
||||
| Testability | 4 | 5 | All DoDs measurable, msw recorder named, concurrency+rate-limit+idempotency tests specified, optimizer pure-function seam, Vitest+Bun setup included in Stage 1. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Critical files / paths (unchanged from v1 + additions)
|
||||
|
||||
[Existing list preserved]
|
||||
|
||||
Additions:
|
||||
- `test/corpus/private/` — sanitizer test SSoT
|
||||
- `docs/linkedin-apps.md` — Dev Portal app IDs + approval states
|
||||
- `docs/a11y-checklist.md` — manual A11y checklist
|
||||
- `db/migrations/README.md` — migration runbook (expand-then-contract sequences)
|
||||
- `docs/marketing-baseline-2026-04.json` — `/market audit` baseline
|
||||
|
||||
---
|
||||
|
||||
## 7. Verification (expanded per TEA gaps)
|
||||
|
||||
- **Unit:** Vitest + Bun workspace. 100% line coverage on pure cores.
|
||||
- **Integration:** real Postgres (Docker), real Redis, `msw` fakes for LinkedIn. Frozen clock via `@sinonjs/fake-timers`; seeded RNG.
|
||||
- **Contract:** msw-recorded LinkedIn API exchanges at `test/fixtures/linkedin/*.har`. **Re-record policy: rotate fixtures monthly or when LinkedIn-Version header bumps** (TEA gap 2). Stale-fixture detector: fixture file mtime > 45 days → CI warning.
|
||||
- **E2E:** Playwright + `@axe-core/playwright` + NVDA scripting on Windows GH Actions runner.
|
||||
- **Security:** `bun audit` in CI; gitleaks pre-commit; Dependabot.
|
||||
- **Load:** k6 at Phase 1 exit — 50 queued publishes drained within SLA.
|
||||
- **DR:** weekly pg_dump-to-Neon rehearsal with row-count verification.
|
||||
|
||||
---
|
||||
|
||||
## 8. Authorization gates — ANSWERED
|
||||
|
||||
| # | Question | Answer (2026-04-19) |
|
||||
|---|---|---|
|
||||
| 1 | Stargue LinkedIn Company Page URL | `https://www.linkedin.com/company/2605890/` — URN `urn:li:organization:2605890` |
|
||||
| 2 | Cadence bootstrap | Confirmed: Tue + Thu ~08:30 AM AST |
|
||||
| 3 | Admin auth | Shared Authentik instance (same for all Stargue services) |
|
||||
| 4 | MDP signatory | Angelo as individual; Stargue entity later |
|
||||
| 5 | Postgres + Neon | PaaS cluster + nightly pg_dump to Neon free tier (RPO ~24h) |
|
||||
| 6 | 4h/week cap override | User override: concentrated full-time build, timeline risk accepted |
|
||||
| 7 | BMAD install location | DQMS charters referenced by path; no install in Publishing Engine repo for this phase |
|
||||
| 8 | Primary operator persona + A11y context (UX gap 8) | **Option C (2026-04-19):** primary = Angelo (VoiceOver-capable); secondary = Claude Code agents (programmatic). A11y target: human-first WCAG 2.2 AA; agents inherit semantic markup for free. |
|
||||
|
||||
---
|
||||
|
||||
## 9. Risks (expanded per panel)
|
||||
|
||||
| Risk | Mitigation | Raised by |
|
||||
|---|---|---|
|
||||
| LinkedIn denies MDP | 60-day re-auth operational baseline; proactive T-5d notification | plan v1 |
|
||||
| **LinkedIn denies Community Management API** | Personal-profile posting unblocked; org-posting tools built but gated until approval | dev |
|
||||
| **LinkedIn algorithm suppresses automated-posted content reach** | Monitor engagement delta between automated vs manual-posted baselines for first 4 weeks; if suppression detected, fallback to draft-and-prompt workflow via admin | **analyst** |
|
||||
| LinkedIn suspends app (ToS) | Conservative rate limit; approval on every publish; no messaging; no engagement automation; own-content only | v1 |
|
||||
| Refresh token revoked mid-series | Fail-safe halt + Telegram notification within minutes | v1 |
|
||||
| Refresh rotation race | Advisory lock on refresh path (TEA gap 3) | tea |
|
||||
| Duplicate post via retry | Idempotency key on publications + BullMQ jobs (dev gap 4) | dev, tea |
|
||||
| Dokploy outage during scheduled post | DLQ + exponential backoff + alert | v1 |
|
||||
| Private vault leak via sanitizer gap | 3-layer fail-closed + CI corpus (sanitize gap 8) | v1, TEA |
|
||||
| OneDrive sync conflict | Scheduler reads from Postgres, not OneDrive; checksum on ingest | v1 |
|
||||
| Admin PII leak | No PII in dashboard; Authentik SSO; audit view | v1 |
|
||||
| Bad LinkedIn post past 5-min edit window | Runbook: delete via `linkedin_delete_post` tool; audit; post-incident review documented in `docs/incidents/` (architect, SM) | architect, sm |
|
||||
| Scope creep | Stages gated by measurable DoD; Phase 2 = separate plan | v1 |
|
||||
| **4h/week → full-time override creates personal-capacity risk** | User-owned timeline risk; checkpoints at Stage 3, 5, 7 to re-evaluate (PM gap 1) | pm |
|
||||
| **MDP + Community Management API approval timeline unknown** | Plan works without either; track application IDs in `docs/linkedin-apps.md` | pm |
|
||||
| DPIA for OAuth-token processing of natural person (Angelo) | **DPIA recorded in `docs/dpia/linkedin-oauth.md`** — purpose (publication automation), data (encrypted tokens only), retention (lifetime-of-authorization), lawful basis (contract-performance + legitimate interest), user rights (erasure via token revoke + hard-delete row) (TEA gap PbD) | tea, sm |
|
||||
|
||||
---
|
||||
|
||||
## 10. Out of scope (deferred to Phase 2 with separate plan)
|
||||
|
||||
- Multilingual routing + hreflang (PAP/NL/ES) — F-021. **Tradeoff explicit:** 7 Cs is EN-only at launch; PAP-speaking stakeholders see EN during bootstrap. Phase 2 adds language-aware routing (Analyst + PM gaps).
|
||||
- Medium, Substack, ResearchGate, X, Reddit, Telegram, TikTok, Facebook, WhatsApp, Newsletter-email outlets — F-013, F-033
|
||||
- Inlet Engine — F-030, F-031 (separate PRD)
|
||||
- Cross-platform analytics beyond LinkedIn + stargue.com/net
|
||||
- Higgsfield media pipeline
|
||||
- Library-vs-MCP decision (scheduler uses library directly; MCP reserved for Claude Code — if future needs require HTTP transport, separate plan)
|
||||
|
||||
---
|
||||
|
||||
## 11. Open questions — ALL RESOLVED
|
||||
|
||||
All pre-execution gates answered 2026-04-19. Proceeding to Stage 0.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[Publishing Engine PRD]]
|
||||
- [[7 Cs LinkedIn Series - Continuation Plan]]
|
||||
- [[Design Philosophy - The Nine Laws of the Hearth]]
|
||||
- [[Outlet Profiles/LinkedIn]]
|
||||
- [[Implementation Progress]]
|
||||
|
||||
## Sources
|
||||
|
||||
- [LinkedIn Programmatic Refresh Tokens (Microsoft Learn, retrieved 2026-04-19)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens)
|
||||
- [LinkedIn 3-Legged OAuth Flow (Microsoft Learn, retrieved 2026-04-19)](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow)
|
||||
- [LinkedIn Posts API (Microsoft Learn)](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/shares/posts-api) — current replacement for deprecated UGC Posts
|
||||
- `/home/devuser/projects/dqms/.clinerules/12-foundational-principles.md`
|
||||
- `/home/devuser/projects/dqms/.claude/hooks/gate-plan-exit.sh`
|
||||
- `/home/devuser/projects/dqms/.claude/skills/bmad-plan/SKILL.md`
|
||||
- `/home/devuser/projects/dqms/_bmad/bmm/agents/*.md` — 7-agent charters (SSoT for panel roles)
|
||||
- Panel verdicts v2: `/tmp/bmad-panel-verdicts-publishing-engine-phase1-20260419.json`
|
||||
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"plan_file": "c:\\Users\\aluid\\OneDrive\\Documents\\Obsidian\\Angelo'sSBO\\Stargue\\Projects\\Publishing Engine\\Plan - Phase 1 Automated Publishing Engine.md",
|
||||
"created_at": "2026-04-19T10:20:00-04:00",
|
||||
"applicable_tier2": ["RBR", "A11y", "Testability"],
|
||||
"verdicts": [
|
||||
{
|
||||
"agent": "analyst",
|
||||
"verdict": "APPROVE",
|
||||
"confidence": "medium",
|
||||
"summary": "Plan is evidence-grounded and principle-aligned; approve subject to resolving Company-Page existence, timeline capacity, Bayesian spec, and LinkedIn algorithmic-reach competitive risk before Stage 3.",
|
||||
"gaps": [
|
||||
"§4 timeline: 7-day schedule is aggressive given PRD §2 production-problem framing; no FTE/capacity assumption stated",
|
||||
"§2/§8: Stargue Company Page URL is both an Objective and an open gate — stakeholder alignment gap",
|
||||
"§3.7 cadence optimizer: Bayesian posterior under-specified (prior, thresholds, sample-size beyond ≥8)",
|
||||
"§10: multilingual (PAP/NL/ES) deferred, but PRD §5.3/NF-008 and 7 Cs plan assume PAP audience",
|
||||
"§9: no competitive risk entry for LinkedIn algorithm/reach-suppression of automated-posted content"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "§3.1 monorepo with packages/{sanitize,schema,linkedin-client,observability} gives clear single-responsibility boundaries; scheduler + MCP both consume linkedin-client (no duplication)."},
|
||||
"SSoT": {"score": 5, "rationale": "§3.4 sanitize config + §3.5 Drizzle schema + §5 design tokens generated from Nine Laws doc — each concept has one authoritative location."},
|
||||
"FPT": {"score": 4, "rationale": "§1 cites retrieved LinkedIn MDP doc with date; §3 stack choices match verified stargue-com/stargue-net inventory. Minor: Bayesian optimizer under-specified."},
|
||||
"DiDSP": {"score": 5, "rationale": "§3.3 libsodium sealed-box tokens, Authentik SSO, bearer tokens scheduler↔MCP, rate limits, no public exposure, Zod at every boundary."},
|
||||
"PbD": {"score": 4, "rationale": "§5 notes token minimization, 13-month metrics retention, erasure path via token revoke + git delete. Gap: no DPIA note on LinkedIn-side audience data."},
|
||||
"OF": {"score": 5, "rationale": "§3.3 correlation IDs, Pino→OpenObserve, PII-redacted content (length+hash only), append-only audit table, proactive T-5d auth-expiry notification."},
|
||||
"RBR": {"score": 4, "rationale": "§3.3 fail-safe halt, §3.7 human-in-the-loop for cadence, §3.5 expand-then-contract, §4.3 DLQ. Gap: no rollback drill for published-to-LinkedIn content past 5-min edit window."},
|
||||
"A11y": {"score": 4, "rationale": "§3.6 WCAG 2.2 AA, axe-core in CI, keyboard parity, prefers-reduced-motion, contrast on both themes; OAuth screen delegated to LinkedIn. Gap: no screen-reader manual test named."},
|
||||
"Testability": {"score": 4, "rationale": "§3 pure core in packages; §7 contract tests against recorded LinkedIn exchanges; injectable clock/ID implied. Gap: cadence-optimizer seams not explicit."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "architect",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Architecturally sound and stack-verified, but over-partitioned for a 7-day build — collapse api-into-admin, fix the libsodium primitive, and cite the rate-limit numbers before Stage 0.",
|
||||
"gaps": [
|
||||
"§3.1 Repo topology adds Turbo + 4 apps + 4 packages for a 7-day plan — architecturally heavy for one-engineer rollout; suggest collapsing apps/api into apps/admin route handlers",
|
||||
"§3.3 MCP transport: Streamable HTTP spec version unnamed; scheduler-to-MCP over HTTP within same Docker network adds a hop vs direct library import — justify or use library",
|
||||
"§3.3 Token store: libsodium sealed-box is public-key encryption; for symmetric at-rest DB encryption use secretbox or KMS envelope",
|
||||
"§3.3 Rate limit numbers (~100/day per member) are uncited",
|
||||
"§3.8 Postgres: says 'shared with other PaaS apps (existing cluster)' — verify; §8.5 open question contradicts",
|
||||
"§4 Stages: 7-day timeline for OAuth×2 identities + 8 MCP tools + scheduler + admin + A11y CI + E2E is optimistic",
|
||||
"§3.4 Sanitization: no test corpus named in Stage DoD (only in §9 risks)",
|
||||
"§3.6 Admin: no CSRF strategy named for approval POSTs"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Clean package boundaries (§3.1); mcp-linkedin consumes linkedin-client — but api+admin separation may violate YAGNI for Phase 1."},
|
||||
"SSoT": {"score": 5, "rationale": "Frontmatter schema, blocklist, design tokens each single-homed (§3.4, §3.5, §3.6)."},
|
||||
"FPT": {"score": 4, "rationale": "Stack verified against actual repos; LinkedIn docs cited with retrieval date. Loses a point on uncited rate-limit figures and MCP transport choice."},
|
||||
"DiDSP": {"score": 4, "rationale": "Zod+Authentik+rate-limit+encrypted tokens+audit (§3.3) — solid. Loses point for libsodium sealed-box mis-selection and missing CSRF."},
|
||||
"PbD": {"score": 4, "rationale": "Low PII surface, 13-month retention, erasure path named (§5). Token retention policy could be more explicit."},
|
||||
"OF": {"score": 5, "rationale": "Correlation IDs end-to-end, OpenObserve integration, body-hash redaction, append-only audit (§3.3, §5)."},
|
||||
"RBR": {"score": 4, "rationale": "Expand-only migrations, DLQ, human-gated cadence, fail-safe on auth. LinkedIn delete only within 5-min edit window — no compensating audit-before-publish preview enforcement."},
|
||||
"A11y": {"score": 4, "rationale": "WCAG 2.2 AA target, axe-core in CI, keyboard parity, both themes contrast-checked. Screen-reader manual test unlisted in DoD."},
|
||||
"Testability": {"score": 4, "rationale": "Pure core in packages, injectable clock, client behind interface, contract tests against recorded exchanges. No named fake for token-store rotation race."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "pm",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Strong governance and architecture, but the plan inverts the PRD's revenue-first pivot and fails to reconcile scope with the 4h/week capacity cap — revise scope and timeline before exit.",
|
||||
"gaps": [
|
||||
"§4 Stages vs PRD §6.5: Plan compresses M1.1–M1.4 (PRD 7 weeks @ 4h/wk = 28h) into ~7 days with no hours-per-stage stated; PRD §9 risk #1 is solo-founder capacity",
|
||||
"§2 Objectives: zero mention of revenue conversion (PRD §6.4, F-003); PRD v2 pivot explicitly revenue-first, plan adds infrastructure only",
|
||||
"§10: multilingual (PAP/NL/ES) deferred to Phase 2 but PRD F-021 places at P1 and 7 Cs plan publishes EN only",
|
||||
"§4 Stage 7: 14 drafts committed in 2 days assumes autoresearch loop is near-free — no hours estimate",
|
||||
"§3.6/§3.7 Admin + optimizer are M1.2/M2 scope in PRD; pulling into Phase 1 redefines Phase 1 boundary",
|
||||
"§11: MDP application timeline not flagged for revenue-dependent features"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Monorepo boundaries clean (§3.1); standard packages/apps split; no leaky abstractions."},
|
||||
"SSoT": {"score": 5, "rationale": "Frontmatter schema, sanitize config, design tokens all single-location; eliminates existing ad-hoc parse."},
|
||||
"FPT": {"score": 4, "rationale": "Cites LinkedIn MDP docs with retrieval date; verified repo state; real outlet profile. Does not probe 4h/wk capacity math."},
|
||||
"DiDSP": {"score": 5, "rationale": "Zod at boundaries, Authentik SSO, libsodium, rate limiting, audit table, no public MCP exposure."},
|
||||
"PbD": {"score": 4, "rationale": "Token retention + encryption + revocation path; 13-month analytics cap; content is public-by-design."},
|
||||
"OF": {"score": 5, "rationale": "Correlation IDs through API→scheduler→MCP; OpenObserve dashboards; audit append-only; PII redaction."},
|
||||
"RBR": {"score": 4, "rationale": "Human approval on every publish, DLQ, expand-only migrations, feature flag on adaptive optimizer, Telegram fail-safe. No staged rollout for MCP server itself."},
|
||||
"A11y": {"score": 4, "rationale": "WCAG 2.2 AA target, axe-core CI, keyboard parity, prefers-reduced-motion, dual-theme contrast. Not yet tested with SR."},
|
||||
"Testability": {"score": 4, "rationale": "Pure core in packages, injectable clock/ID, LinkedIn client behind interface with contract tests. Sandbox-or-contract fallback right."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "sm",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Architecture and DoDs are largely sprint-ready, but stages must be re-timelined against the PRD's 4h/week cap and 6 DoD items need 'works'-language tightened before dev handoff.",
|
||||
"gaps": [
|
||||
"§4 DoD uses calendar 'days 1–7' but PRD caps at 4h/week; re-express as Week-1…Week-N at 4h/week or justify",
|
||||
"§4 Stage 1.2 DoD 'Sanitizes sample vault note correctly' needs replacement with 'round-trips 12-file private-corpus fixture with 0 leaks and 12 build errors'",
|
||||
"§4 Stage 3.4: no cleanup step; add 'test post deleted within 5-min edit window; deletion audited'",
|
||||
"§4 Stage 4.2 'End-to-end happy path': specify trace (POST /content → approval → scheduled_at → git SHA + LinkedIn URN recorded, status=published, within 120s)",
|
||||
"§4 Stage 5.3 'No violations above minor' needs axe-core severity gate (zero serious+critical) + WCAG contrast ratios",
|
||||
"§4 Stage 7.4 'Observed live' not CI-demonstrable; add 'publication row external_url returns HTTP 200 from unauthenticated curl; metrics row at T+48h±1h'",
|
||||
"§8 Auth gates block Stage 0 exit but Stage 0 has no 'gates answered' DoD row",
|
||||
"§4 Stage 2.1 'expand-only migrations documented': needs pointer to doc location",
|
||||
"No sprint sequencing artifact / story backlog — charter requires it for dev handoff"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Clean monorepo boundaries (§3.1); shared packages reused — no duplicated logic named."},
|
||||
"SSoT": {"score": 5, "rationale": "§3.4 + §3.5 put frontmatter, sanitize config, tokens in single packages; §5 SSoT row cites file paths."},
|
||||
"FPT": {"score": 4, "rationale": "§1 cites LinkedIn MDP doc with retrieval date; verified repo state; rejected n8n with tradeoff. Miss on Phase 0 capacity constraint."},
|
||||
"DiDSP": {"score": 5, "rationale": "§3.3 token encryption + §3.5 audit table + §3.6 Authentik SSO + §9 rate limits — layered, concrete."},
|
||||
"PbD": {"score": 4, "rationale": "§5 PbD row names retention (13mo), erasure path (token revoke + hard delete), purpose limitation. Missing DPIA note."},
|
||||
"OF": {"score": 5, "rationale": "§3.3 per-tool correlation ID + OpenObserve + redaction rule — structured and specific."},
|
||||
"RBR": {"score": 3, "rationale": "§5 names approval-gate + DLQ + expand-contract + flagged adaptive cadence. Missing rollback runbook for a bad LinkedIn post past 5-min edit window."},
|
||||
"A11y": {"score": 4, "rationale": "§3.6 + Stage 5.3/5.4 name axe-core, keyboard parity, reduced-motion, dual-theme contrast. 'No violations above minor' ambiguous."},
|
||||
"Testability": {"score": 4, "rationale": "§7 splits unit/integration/E2E; §3.3 client behind interface; injectable clock mentioned. Cadence-optimizer Bayesian logic has no named test seams."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "ux-designer",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Strong security/SSoT/observability spine with Nine-Laws-aware design intent, but A11y is declared-not-designed — add operator flows, SR protocol, dual-theme contrast verification, and reconcile Law-3 temperature with WCAG 1.4.1 before exit.",
|
||||
"gaps": [
|
||||
"§3.6/§5: A11y names mechanisms but omits user flows; no wireframes/task analysis/cognitive-load budget for /queue approval task (where slip leaks private content)",
|
||||
"§3.6: aria-live polite/assertive level unspecified; no error-association spec for OAuth re-auth or approval form (WCAG 3.3.1, 3.3.3, 4.1.3)",
|
||||
"§3.6: no screen-reader testing in CI or manual protocol (charter requires SR-tested for score 5)",
|
||||
"§3.6: dual-theme contrast promised but neither theme's token values (warm #fefbf6 / cool #f8fafc + Forest/Lime accents) verified against 4.5:1 / 3:1",
|
||||
"Nine Laws fit: admin dashboard is operator-tool — no temperature declared; risk of Law-4 amber leakage into status badges",
|
||||
"§3.7 cadence optimizer heatmap is color-encoded — Law 3 temperature semantics conflict with WCAG 1.4.1 (color not sole means)",
|
||||
"§4 Stage 5: no criterion for focus-trap on modal dialogs, skip-links, or reduced-motion honoring in section fade-in (P8.4)",
|
||||
"§11: open questions omit 'primary operator persona and accessibility context'"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Monorepo boundaries in §3.1 are clean; shared packages reused by scheduler + mcp; tradeoff named."},
|
||||
"SSoT": {"score": 5, "rationale": "§3.1 shared packages, §3.4 Zod config, §3.6 design tokens inherited from Nine Laws doc — no duplication path."},
|
||||
"FPT": {"score": 4, "rationale": "§1 cites LinkedIn docs with retrieval date and verifies repo state; rejects n8n on SSoT grounds."},
|
||||
"DiDSP": {"score": 5, "rationale": "§3.3 sealed-box, §3.4 fail-closed sanitizer, §3.5 append-only audit, Authentik + bearer layered."},
|
||||
"PbD": {"score": 4, "rationale": "§3.3 token minimization, 13-month analytics retention, erasure path named."},
|
||||
"OF": {"score": 5, "rationale": "§3.3/§3.5 correlation IDs, structured Pino logs, OpenObserve dashboards, append-only audit."},
|
||||
"RBR": {"score": 4, "rationale": "§3.7 human-in-loop, DLQ, expand-only migrations, feature flag on adaptive cadence."},
|
||||
"A11y": {"score": 3, "rationale": "Targets WCAG 2.2 AA and names axe-core/keyboard/contrast — missing SR testing, user flows, cognitive-load analysis on approval task, heatmap color-only encoding risk, Law-3/Law-4 contrast verification. Meets floor, not exemplary."},
|
||||
"Testability": {"score": 4, "rationale": "§7 names unit/integration/E2E/contract tests; §3.3 injectable token store; pure-core in packages."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "dev",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Solid architecture and governance, but ship-blocker inaccuracies on LinkedIn auth (PKCE unverified) and endpoint (UGC Posts is Legacy; use Posts API with LinkedIn-Version header) must be corrected before Stage 3.",
|
||||
"gaps": [
|
||||
"§3.3 Auth: scopes 'openid profile email' are from 'Sign In with LinkedIn using OpenID Connect' product; 'w_member_social' requires 'Share on LinkedIn' product — must be declared per-product in Dev Portal. 'w_organization_social'/'r_organization_social' require Community Management API product approval (separate gate).",
|
||||
"§3.3 Auth: PKCE is NOT documented as supported by LinkedIn's 3LO — Microsoft Learn authorization-code-flow shows only response_type=code + client_secret. Either cite PKCE support or drop it.",
|
||||
"§3.3 Tools: UGC Post API is now Legacy; must use Posts API (/rest/posts) with 'LinkedIn-Version: YYYYMM' header and 'X-Restli-Protocol-Version: 2.0.0'. Building against /v2/ugcPosts = tech debt on day one.",
|
||||
"§3.2/§3.5: no idempotency key on POST /content or BullMQ job dispatch — retries could double-post",
|
||||
"§3.8: stargue-net/package.json is byte-identical to stargue-com/package.json (same 'name' field) — name collision to resolve before workspace link",
|
||||
"§4 Stage 3.9: MCP Streamable HTTP transport is current spec (stdio+SSE deprecated); pin @modelcontextprotocol/sdk version",
|
||||
"§7: no AC→Vitest mapping; contract-test recording tool unchosen (Polly.js? msw?)"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Clean package boundaries; MCP client reused by scheduler; no duplicated domain logic across apps."},
|
||||
"SSoT": {"score": 4, "rationale": "Zod schema + sanitize config + design tokens each single-homed. Risk: stargue-net package.json duplicates stargue-com verbatim — name collision to resolve."},
|
||||
"FPT": {"score": 3, "rationale": "Plan cites repo state and doc retrieval date, but LinkedIn API claims don't match live docs: UGC Posts is Legacy (Posts API current), PKCE not documented as supported. Fix before impl."},
|
||||
"DiDSP": {"score": 4, "rationale": "Authentik SSO + internal bearer + Zod + libsodium + rate limits + audit log + no public MCP exposure. Missing idempotency key to prevent replay/double-post."},
|
||||
"PbD": {"score": 4, "rationale": "Low PII surface; token retention tied to lifetime; analytics capped 13mo; erasure path named."},
|
||||
"OF": {"score": 5, "rationale": "Correlation IDs across API→scheduler→MCP; Pino JSON; OpenObserve; append-only audit; per-call latency+outcome; PII redaction."},
|
||||
"RBR": {"score": 4, "rationale": "Approval gate + DLQ + expand-then-contract + feature flag + proactive re-auth notification."},
|
||||
"A11y": {"score": 3, "rationale": "WCAG 2.2 AA + axe-core CI + keyboard parity + reduced-motion + dual-theme contrast. Lacks SR test plan and aria-live specifics."},
|
||||
"Testability": {"score": 3, "rationale": "Pure core extracted; injectable clock/ID; client behind interface. Missing AC→test mapping, HTTP-recording lib, and Vitest setup for Bun workspaces."}
|
||||
}
|
||||
},
|
||||
{
|
||||
"agent": "tea",
|
||||
"verdict": "REVISE",
|
||||
"confidence": "high",
|
||||
"summary": "Architecture is sound and SSoT/OF are exemplary; revise §4 DoDs into measurable gates, close refresh-race + idempotency + migration-rehearsal + kill-switch gaps, then re-score — currently REVISE.",
|
||||
"gaps": [
|
||||
"§4 Stage DoDs largely untestable as stated ('Smoke tests pass', 'Test post lands', 'End-to-end happy path on staging', 'Matches stargue.com visual system') — add measurable criteria",
|
||||
"§7 omits flake controls: no frozen clock, seeded RNG, deterministic IDs, or VCR/contract-test refresh cadence",
|
||||
"§3.3 refresh path lacks concurrency guard — two scheduler jobs on expired token simultaneously can double-rotate; specify SELECT FOR UPDATE / advisory lock",
|
||||
"§3.5 'expand-only' migrations claimed but no shadow-column/backfill/contract sequence shown; add migration rehearsal DoD",
|
||||
"§4 Stage 4 DLQ DoD (4.3) names backoff but not idempotency key — retries could duplicate public posts",
|
||||
"§4 Stage 3 lacks rate-limit test DoD — add burst-21-calls test asserting 429 on #21",
|
||||
"§3.6 A11y: axe-core 'baseline' and 'no violations above minor' is vague — specify ruleset version, severity gate, SR smoke",
|
||||
"§3.4 sanitizer: no DoD for the private-path corpus itself — add 'CI fails if test/corpus/private/* round-trips without build error'",
|
||||
"§8 gate #3 assumes Authentik exists — Stage 5.2 DoD circular if not",
|
||||
"No kill-switch / feature flag for LinkedIn poster — RBR requires fast-disable independent of code deploy"
|
||||
],
|
||||
"principles": {
|
||||
"SEBP": {"score": 4, "rationale": "Clean package/app split (§3.1); client shared between scheduler+MCP; tradeoff acknowledged."},
|
||||
"SSoT": {"score": 5, "rationale": "Zod schema, sanitize config, design tokens each in one package."},
|
||||
"FPT": {"score": 4, "rationale": "Cites LinkedIn docs with retrieval date (§1); stack matches verified repo state; refresh-concurrency not reasoned from first principles."},
|
||||
"DiDSP": {"score": 4, "rationale": "Layered (Zod+Authentik+bearer+libsodium+audit) but idempotency + refresh-race gaps reduce from 5."},
|
||||
"PbD": {"score": 4, "rationale": "Token min/encrypt/retention named; 13-month metrics retention stated. SAR path asserted, not DoD-tested."},
|
||||
"OF": {"score": 5, "rationale": "Correlation IDs end-to-end, append-only audit, PII redaction, per-call LinkedIn latency capture."},
|
||||
"RBR": {"score": 3, "rationale": "Approval gate, DLQ, proactive auth notify present — but expand-contract migrations unspecified, no idempotency key, no runtime kill-switch."},
|
||||
"A11y": {"score": 3, "rationale": "WCAG 2.2 AA named, axe-core in CI, two-theme contrast — severity gate + SR smoke + ruleset version unspecified."},
|
||||
"Testability": {"score": 3, "rationale": "Pure core extracted, injectable clock/ID/randomness, client-behind-interface — but Stage DoDs largely untestable and no determinism/record-refresh policy."}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user