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
15 KiB
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_FILEoverride; mirrored indata-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,/calendarpublish. Readers: SessionStart hook,/analyze,/report, content-planner. - Lifecycle:
## Recent Postsauto-pruned at 90d on session-start (state-updater.mjs:138). Streak resets if post gap >2d (:94). Week rollover resetsposts_this_weekviaweek-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.
newestCaptureDatedrives 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 — invarianttypes.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/newsletterStep 1.5 elicitation. Readers:queryByTopic(:128) — excludes archived. Companionsbinding.ts(Step 2.5 gate),kilder.ts. - Lifecycle: Dedup on content; re-capture unions tags. Archive = soft-hide. No time-prune.
verificationgates numbers. - Survives reinstall: YES.
D. Analytics store
- Location root:
${data}/analytics/viagetAnalyticsRoot()(scripts/analytics/src/utils/storage.ts:67) — through seam; legacyANALYTICS_ROOThonored (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— fileposts/YYYY-MM-DD-<8char>.jsoninsideAnalyticsBatch(batchId,importedAt,exportFilename,dateRange{from,to},postCount,posts[]).PostMetrics=impressions,reactions,comments,shares,clicks,engagementRate,saves?— nodwell(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); genuine0kept; not folded intoengagementRate; surfaced only when ≥1 post carries it.- Writers: analytics CLI (
cli.ts) —import→posts/,report→weekly/monthly; ab-tests written by/ab-testprose. 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:10join(getDataRoot('drafts'),'queue.json')). Migratedmigrate-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/draftviaqueueUpdateStatus),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(migratedmigrate-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). Onlyweek-*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 atassets/voice-samples/authentic-voice-samples.mdwith<!-- 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 byvoice-scrubber). - Resolution:
personalization-score.mjspick(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 fromconfig/user-profile.local.md,migrate-data.mjs:34; template seedconfig/user-profile.template.md). Scored 20pts when <3[Yourplaceholders 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; templateconfig/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:
- 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. - 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) → aPostAnalyticsrow (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
- 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. - 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.
- Two roots + legacy-alias window. Reconcile
getStateFile()withgetDataRoot()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. - 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.
- 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.