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

15 KiB
Raw Permalink Blame History

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.mdNOT 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.mjsupdatePostTracking (: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).
  • 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) — importposts/, 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.
  • 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.mjsstorage.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.