linkedin-studio/docs/second-brain/research/silo-inventory.md
Kjell Tore Guttormsen d3199eb997 docs(linkedin-studio): second-brain architecture (approved) + 3 research reports
Persist the operator-approved second-brain architecture design and the three
parallel research threads that fed it, so nothing is lost before build:

- architecture.md — approved design: thin Markdown `brain/` hub (two-layer
  profile.md + episodic journal/ + operations.md + index.md MOC) over the
  existing typed tributaries (voice/specifics/trends/analytics), a
  provenance-tagged ingest/ seam, a sleep-time consolidation loop with
  evidence-threshold promotion + temporal-validity + anti-sycophancy/
  anti-collapse invariants. Build sequence SB-S0..S4.
- research/connector-egress.md — LinkedIn data egress reality (EU/EEA DMA
  portability API = auto for content; analytics manual CSV; no scraping).
- research/secondbrain-sota.md — 2026 second-brain / AI-memory SOTA synthesis.
- research/silo-inventory.md — faithful inventory of the 12 existing per-user
  silos + the 5 hardest unification problems.

Boundary confirmed: engine -> plugin (domain-general), user data -> data dir,
cockpit -> Maskinrommet. Design phase, no code yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
2026-06-23 12:55:17 +02:00

110 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<weekly|monthly>-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 `<!-- VOICE_PLACEHOLDER -->` 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)
AL 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`.