Files
stargue-publishing-engine/docs/plans/2026-04-19-phase1-plan.md
Angelo B. J. Luidens 1dc1a1a07a 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>
2026-04-19 07:22:07 -04:00

31 KiB
Executable File
Raw Blame History

created, updated, tags
created updated tags
2026-04-19T09:45 2026-04-19T10:45

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). 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 27 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, 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 blocklistFamily 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 18)

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 14, 68; 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
  • postgresconfirm 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 23)

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 (chosen recorder, dev gap 7) against recorded Posts API fixtures

Stage 2 — Database + core API (days 34)

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 47)

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 78)

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 810)

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 1114)

Step Measurable DoD
7.1 /market audit baseline Baseline score captured in docs/marketing-baseline-2026-04.json
7.2 Draft 7 Cs posts 28 (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.


Sources