# Research — Per-User Data Silo Inventory (LinkedIn Studio, 2026-06-23) > Research thread `silo-inventory`, 2026-06-23. Every silo read from the actual files (TS types/store code, .mjs writers, templates, command/agent prose), not inferred. Research INPUT to `../architecture.md`. Preserved so nothing is lost. (file:line refs were accurate at capture time — re-confirm before editing those lines.) The unifying seam is the **data-path convention** (`hooks/scripts/data-root.mjs:24` `getDataRoot()` / `scripts/analytics/src/utils/storage.ts getDataRoot`), default `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/`. Twin implementations (Node `.mjs` for hooks, TS for analytics/trends/specifics CLIs) because runtimes differ — `data-root.mjs:1-8` documents the must-stay-in-sync contract. The **M0 migration manifest** (`hooks/scripts/migrate-data.mjs:30-54`) is the single source of truth for what moves out of the plugin tree on session-start. ## The silos ### A. State file (`## Recent Posts`, streaks, milestones, first-hour, outreach) - **Location:** `~/.claude/linkedin-studio.local.md` — **NOT** under the data-root seam. Own resolver (`state-updater.mjs:11-12`, `STATE_FILE` override; mirrored in `data-root.mjs:30 getStateFile()`). The data root "mirrors" this file (`data-path-convention.md:15`) but they are two separate locations. - **Format:** YAML frontmatter + markdown sections. - **Schema** (`config/state-file.template.md`): frontmatter — `last_post_date`, `first_post_date`, `last_post_topic`, `posts_this_week`, `weekly_goal`, `current_streak`, `longest_streak`, `current_week`, `last_import_date/week`, `follower_count/target`, `target_date`, `monthly_growth[]`, `projected_10k_date`, `growth_rate_needed`, `next_planned_topic`, `pending_5x5x5`, `content_series_active`, `last_firsthour_date`, `firsthour_active`, `last_outreach_date`, `outreach_active`, `expertise_areas[5]`. Sections — `## Recent Posts` (`- [YYYY-MM-DD] "hook…" (chars) - topic`), `## Session Notes`, `## Planned Content`, `## Milestone Log` (`- [YYYY-MM] count (+delta)`), `## First-Hour Plans`, `## Outreach Pipeline`. - **Writers:** `state-updater.mjs` — `updatePostTracking` (:55), `updateFollowerCount` (:181), `recordFirstHourPlan` (:235), `recordOutreachContact` (:294), `pruneContentHistory` (:138). From Stop hook + `/firsthour`, `/outreach`, `/calendar` publish. **Readers:** SessionStart hook, `/analyze`, `/report`, content-planner. - **Lifecycle:** `## Recent Posts` auto-pruned at 90d on session-start (`state-updater.mjs:138`). Streak resets if post gap >2d (:94). Week rollover resets `posts_this_week` via `week-rollover.mjs`. - **Survives reinstall:** YES (in `~/.claude/`, not plugin tree). ### B. Trends store - **Location:** `${data}/trends/trends.json` — through seam (`scripts/trends/src/store.ts:180 defaultStorePath()`). - **Format:** JSON (TS-managed), `{ schemaVersion:1, trends:TrendRecord[] }`. - **Schema** (`trends/src/types.ts:26`): `TrendRecord` = `id` (sha256[:12] of normalized title+url = dedupe key), `title`, `url`, `source`, `capturedAt` (ISO), `topics[]`, `summary?`. - **Writers:** CLI `addTrend` (`store.ts:111`), populated by trend-spotter. **Readers:** `queryByTopic` (:137), `history` (:156), `newestCaptureDate` (:171). - **Lifecycle:** Dedup on title+url; re-capture **unions topics**, keeps first-sight provenance. `newestCaptureDate` drives the SessionStart trend-freshness nudge (B-S3). No prune. - **Survives reinstall:** YES. ### C. Lived-specifics bank - **Location:** `${data}/specifics-bank/specifics-bank.json` — through seam (`scripts/specifics-bank/src/bank.ts:150 defaultBankPath()`). - **Format:** JSON (TS-managed), `{ schemaVersion:1, specifics:Specific[] }`. - **Schema** (`specifics-bank/src/types.ts:46`): `Specific` = `id` (sha256[:12] of normalized content = dedupe key), `type` (`number`|`named-case`|`what-broke`|`contrarian`|`mind-change`|`other`), `content` (operator's own words, never AI-generated — invariant `types.ts:13`), `topicTags[]`, `provenance{capturedAt,source}` (source = edition id/command/"manual"), `verification` (`verified`|`unverified`|`n/a`), `status` (`active`|`archived`). - **Writers:** CLI `addSpecific` (`bank.ts:102`), from `/newsletter` Step 1.5 elicitation. **Readers:** `queryByTopic` (:128) — excludes archived. Companions `binding.ts` (Step 2.5 gate), `kilder.ts`. - **Lifecycle:** Dedup on content; re-capture unions tags. Archive = soft-hide. No time-prune. `verification` gates numbers. - **Survives reinstall:** YES. ### D. Analytics store - **Location root:** `${data}/analytics/` via `getAnalyticsRoot()` (`scripts/analytics/src/utils/storage.ts:67`) — through seam; legacy `ANALYTICS_ROOT` honored (deprecated). Subdirs: `exports/`, `posts/`, `weekly-reports/`, `monthly-reports/`, `ab-tests/`. - **Format:** JSON (TS-managed) posts/reports; CSV input; markdown ab-tests. - **Schema** (`analytics/src/models/types.ts`): `PostAnalytics` = `id`, `title`, `publishedDate`, `metrics`, `importedAt`, `exportSource` — file `posts/YYYY-MM-DD-<8char>.json` inside `AnalyticsBatch` (`batchId`, `importedAt`, `exportFilename`, `dateRange{from,to}`, `postCount`, `posts[]`). `PostMetrics` = `impressions`, `reactions`, `comments`, `shares`, `clicks`, `engagementRate`, `saves?` — **no `dwell`** (intentional). `WeeklyReport`(`weekly-reports/YYYY-WXX.json`)/`MonthlyReport`(`monthly-reports/YYYY-MM.json`) = `summary{totals incl. totalSaves?, avgEngagementRate, avgImpressionsPerPost}`, `topPerformers[]`, `underperformers[]`/`byWeek[]`, `trends{…percentChange}`, `alerts[]`. - **`saves`** (`parseOptionalCount`, `csv-parser.ts:71`): blank/non-numeric/negative→`undefined` (unknown, never 0); genuine `0` kept; **not folded into `engagementRate`**; surfaced only when ≥1 post carries it. - **Writers:** analytics CLI (`cli.ts`) — `import`→`posts/`, `report`→weekly/monthly; ab-tests written by `/ab-test` prose. **Readers:** CLI report/trends/heatmap subcommands; analytics-interpreter agent (no agent re-reads posts JSON directly — report path is CLI-internal). - **Lifecycle:** Posts deduped by post id on import (latest wins). No time-prune. Reports overwrite by key. - **Survives reinstall:** YES. ### E. Drafts queue - **Location:** `${data}/drafts/queue.json` — through seam (`queue-manager.mjs:10` `join(getDataRoot('drafts'),'queue.json')`). Migrated `migrate-data.mjs:32`. - **Format:** JSON, `{ version:1, queue:[] }`. - **Schema** (`queue-manager.mjs:63-79`, the actual writer): `id`, `draft_path`, `scheduled_date`, `scheduled_time`, `pillar`, `format`, `hook_preview`, `character_count`, `status` (`'scheduled'` on add; →`posted`/`archived`/`skipped`/`draft` via `queueUpdateStatus`), `created_at`. - **Writers:** `queueAdd`/`queueUpdateStatus` (:63,:82) — `/batch`, `/calendar`. **Readers:** `queueRead`/`Today`/`Upcoming`/`Overdue` (:41-99), SessionStart, `/calendar`, `/pipeline`, ical-generator. - **Lifecycle:** No prune. Dedup-by-id on add (:64). No week reset. - **Survives reinstall:** YES. ### F. Draft files on disk (week / carousel / multiplatform / repurposed) - **Location (all through seam, confirmed in prose):** `${data}/drafts/week-*/…md` (migrated `migrate-data.mjs:46,129`), `${data}/drafts/carousel-YYYYMMDD-SLUG/slide-N.png` (`commands/carousel.md:133,147`), `${data}/drafts/multiplatform/[platform]-[slug].md` (`commands/multiplatform.md:118`), `${data}/drafts/repurposed/` (`agents/content-repurposer.md:597`). Only `week-*` is in the migration manifest; carousel/multiplatform/repurposed are born external (command/agent prose writes straight to the data root). - **Format:** Markdown (+ YAML frontmatter) / PNG. - **Writers:** creation commands + content-repurposer. **Readers:** `/calendar`, `/pipeline`, repurpose/carousel/video. - **Lifecycle:** No automatic prune — files accumulate; manual cleanup. Dedup by filename/dir slug only. - **Survives reinstall:** YES. ### G. Content plans - **Location:** `${data}/plans/-plan-*.md` — through seam (`agents/content-planner.md:7,40,464`). Born external (not in manifest). - **Format:** Markdown (+ frontmatter: `plan_date`, `generated`, `status`, `posts_planned`, pillars, `content_mix`). - **Writers:** content-planner agent (:464, can also create Linear issues). **Readers:** content-planner (scans prior plans to avoid repetition :40), SessionStart, `/calendar`. - **Lifecycle:** No prune; planner does content-level dedup vs prior plans. **Survives reinstall:** YES. ### H. Voice profile + voice-drift log - **Location:** `${data}/voice-samples/authentic-voice-samples.md` (external canonical; in-plugin **placeholder seed** at `assets/voice-samples/authentic-voice-samples.md` with `` sentinel; migrated MOVE dropping `.local`, `migrate-data.mjs:31`). Plus `${data}/voice-samples/chronicle-voice-drift-log.md` (external-only, no seed; written/read by `voice-scrubber`). - **Resolution:** `personalization-score.mjs` `pick(dataRel,pluginRel)` (:30-32) prefers external, falls back to seed. Voice scores 25pts iff line-count>50 AND no sentinel. - **Format:** Markdown sections (Core Voice Characteristics, Do's/Don'ts, Signature Phrases, Vocabulary, Language Guidelines, Update Log, auto-appended "Collected Post Samples"). - **Writers:** `/setup`/`/onboarding` (overwrite placeholder), voice-trainer (appends samples), voice-scrubber (drift log). **Readers:** `personalization-score.mjs`, `user-prompt-context.mjs`, content commands. - **Survives reinstall:** external instance YES; seed NO (it's the seed). ### I. User profile - **Location:** `${data}/profile/user-profile.md` (migrated MOVE from `config/user-profile.local.md`, `migrate-data.mjs:34`; template seed `config/user-profile.template.md`). Scored 20pts when <3 `[Your ` placeholders remain (`personalization-score.mjs:54`). - **Format/schema:** Markdown — Personalization Settings (identity, expertise areas, audience, goals), Voice & Style, Strategic Context, **Research Tooling** (which MCPs available), Asset Utilization Preferences. - **Writers:** `/setup`/`/onboarding` + manual edit. **Readers:** `personalization-score.mjs`, `user-prompt-context.mjs`, commands route MCPs from it. **Survives reinstall:** external YES. ### J. COPY-class scaffolds (examples / audience-insights / templates) Four files ship a tracked seed + canonical external instance (`migrate-data.mjs:49-54 COPY_FILES`; external never clobbered): `${data}/examples/high-engagement-posts.md`, `${data}/audience-insights/{demographics,engagement-patterns}.md`, `${data}/templates/my-post-templates.md`. Markdown. Written by user/voice-trainer; read by `personalization-score.mjs` (pick-resolved) + content commands. **Survives reinstall:** external YES, seed NO. ### K. content-history.md — vestigial second surface - **Location:** `${data}/analytics/content-history.md` (migrated MOVE, `migrate-data.mjs:33`; template `config/content-history.template.md`). Markdown table (`Date|Hook|Topic|Format|Words|Chars|Source`). - **Status:** No hook/agent code actively writes it — the live content-history surface is the **state file's `## Recent Posts`** (silo A). Migrated-but-superseded scaffold. - **Survives reinstall:** YES (but inactive). ### L. iCal export `${data}/drafts/week-WXX/schedule.ics`, generated from queue by `ical-generator.mjs` (`/batch`). RFC-5545. Transient/regenerated, not a knowledge store. Survives reinstall: YES. ## Synthesis ### Already per-user + reinstall-surviving (the "done" set) A–L all route through the seam or `~/.claude/`. **There is essentially no in-plugin/tmp data left to migrate** — M0 already did it. Only `/tmp/linkedin-hooks/session-active` (a session marker, not data) and the in-plugin **seeds** stay in-tree by design. ### What is NOT yet unified (the real gap) Per-user but **siloed and heterogeneous** — three storage idioms, two roots, no cross-references: 1. **Two roots, not one.** State file at `~/.claude/linkedin-studio.local.md` (own resolver, `state-updater.mjs:12`); everything else under `${data}/`. Unification must decide whether the state file folds in or stays a sibling. 2. **Two writer disciplines.** TS-managed JSON (trends, specifics, analytics) with `schemaVersion` + sha256-id dedup + tested pure functions — **vs.** regex markdown-section mutation (state file, `state-updater.mjs`) — **vs.** schema-less free-form markdown written by command/agent prose (drafts, plans, voice, scaffolds). ### Natural overlaps / relationships - **Posts triple-counted.** A published post lands in three non-referencing places: state `## Recent Posts` (A, live), `analytics/content-history.md` (K, dead), `analytics/posts/*.json` (D, from CSV). Same event, three schemas, no shared id. - **specifics ↔ posts ↔ analytics.** A `Specific` (number/named-case) → a post (F) → a `PostAnalytics` row (D). Nothing links the raw material to the post it seeded or to that post's measured performance — "which raw material performs" is unanswerable today. - **trends ↔ plans ↔ drafts.** trend-spotter captures `TrendRecord` (B); content-planner plan (G) should cite it; draft (F) executes it — all free-text, no id linkage. - **voice ↔ content-history ↔ analytics.** voice-trainer learns from "Collected Post Samples"; the high performers that should train it live in analytics (D) + examples (J) — three disconnected views of "your good posts." - **provenance is the common latent field.** trends (`source`+`capturedAt`), specifics (`provenance{source,capturedAt}`), analytics (`importedAt`,`exportSource`), queue (`created_at`) each independently reinvented capture-provenance under different names. ### The 5 hardest unification problems 1. **Schema-idiom mismatch.** Folding regex-mutated markdown (state file) and prose-written free-form markdown (drafts/plans/voice) into the typed `schemaVersion`+dedup discipline of the JSON stores is the central rewrite. JSON stores (B/C/D) are already second-brain-shaped; the markdown silos are not. 2. **Identity / dedup across silos.** Each store has its own id — sha256(title+url) trends, sha256(content) specifics, hash(title+date) analytics, slug ids queue. **No stable cross-silo entity id**, so the post→specific→trend→analytics graph can't be assembled. Needs one canonical id minted at creation and threaded through. 3. **Two roots + legacy-alias window.** Reconcile `getStateFile()` with `getDataRoot()` while three deprecated aliases (`ANALYTICS_ROOT`/`STATE_FILE`/`PLUGIN_ROOT`) are honored "for one minor version" (`data-path-convention.md:17`) and **two twin implementations** (`data-root.mjs` ⇄ `storage.ts`) must stay byte-compatible. 4. **Triple-recorded post + dead content-history.** K is a migrated-but-unwritten duplicate of A, and neither links to D. Unification must pick one canonical post record and retire/back-fill the other two — a data-reconciliation problem, not just schema. 5. **Provenance normalization + lifecycle divergence.** Each silo reinvented provenance under different field names; retention is inconsistent — 90-day prune (state Recent Posts only), soft-archive (specifics), topic-union-merge (trends/specifics), overwrite-by-key (reports), **no prune at all** for drafts/plans/queue/voice/trends. Needs one provenance shape and one coherent retention policy. Key files: `hooks/scripts/data-root.mjs`, `hooks/scripts/migrate-data.mjs:30-54`, `hooks/scripts/state-updater.mjs`, `hooks/scripts/queue-manager.mjs:10-91`, `scripts/trends/src/{types,store}.ts`, `scripts/specifics-bank/src/{types,bank}.ts`, `scripts/analytics/src/{utils/storage,models/types,parsers/csv-parser}.ts`, `hooks/scripts/personalization-score.mjs:30-152`, `config/state-file.template.md`, `references/data-path-convention.md`.