# M0 — Implementation Log

Running record of decisions, deviations, and out-of-scope follow-ups discovered
during M0 execution. Plan: `docs/m0/plan.md` (18 steps). History → git; this file
captures only what the commit messages cannot.

## Session 4 — Steps 14–18 (2026-06-18)

### Step 14 GATE outcome — the D3 convention works; edit count is ~1:1, not reduced

Prototyped `references/data-path-convention.md` on the voice-readers family:
**38 refs across 19 files repointed** — exactly the plan's prediction. The measured
answer to brief D3's open question (*can a convention reduce edits, or do commands
need literal paths for Claude to act on?*): command/agent prose that tells Claude to
**read** a file needs a resolvable path **on the line**. A "the data dir's
`voice-samples/` (see convention doc)" reference adds a lookup hop and is not directly
actionable. The inline `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/…`
token is both self-resolving **and** points at the doc. So the convention does **not**
cut edit count below ~1 per ref — it makes every edit a **uniform mechanical token
swap** (vs bespoke per-line decisions), with the doc as single source of truth.
**GATE = proceed** (convention confirmed working); Step 15 applies the same uniform
token to the remaining families. This is D3 realized as one token — NOT a re-decision
to literal-edit (alt. a). Applied via an ordered swap (prefixed `${CLAUDE_PLUGIN_ROOT}/
assets/voice-samples/` form before bare `assets/voice-samples/`, so the bare pass can't
corrupt the prefixed one).

### Designed inter-step red lint (Step 14 → Step 16)

After Step 14 the structure lint is **Failed: 1** — `references/*.md: 26 (expected 25)`,
the new convention doc as the 26th ref file. `EXPECT_REFS` bumps to 26 in **Step 16**
(plan Session-4 scope forbids touching tests). This session lands 14→18 in one go, so
the lint is restored to green at Step 16 — no red is left at session end. The only
surviving bare `assets/voice-samples/` is inside the convention doc itself (it documents
the in-plugin placeholder-scaffold location for the fallback rule) — Step 16's
no-bare-path assertion must exempt `references/data-path-convention.md`.

### Step 15 — 130 refs / 34 files repointed; shipped read-only preserved

Rule-based repoint (negative-lookahead per subdir): analytics 50, drafts 24,
audience-insights 14, profile/D1 12, examples 11, plans 6, my-post-templates 4,
frameworks 3, case-studies 3, network 2, repurposing-tracker 1. Counts match the
plan's family estimates once the **shipped exclusions** are accounted for
(frameworks 6→3 drops `framework-template.md`; case-studies 5→3 drops
`case-study-template.md`; analytics drops `README.md`; the Step-11 `ANALYTICS_ROOT`
pins were already gone). **Style-A `${CLAUDE_PLUGIN_ROOT}` preserved** for shipped
read-only: `analytics/README.md`, all `*-template.md` seeds, `assets/checklists/`,
`assets/quick-post-resources.md`, the shipped `assets/templates/*` (only the
`my-post-templates.md` *instance* repointed), `config/*.template.*`, and every
`scripts/analytics` / `hooks/scripts` CODE path. **profile/D1:** `config/user-profile.local.md`
→ `${…}/profile/user-profile.md` (path + filename change, drops `.local`, per
MOVE_FILES). **ab-tests/** routes under `${…}/analytics/ab-tests/` (brief §7.1);
**plans/** at `${…}/plans/` (top-level). Verify: the only in-plugin data-dir paths
left are the three shipped exclusions; full lint Failed:1 = EXPECT_REFS only.

### Step 15 scope notes — two additions, one deferral

- **network/ + repurposing-tracker.md repointed (additions beyond the plan's named
  families).** Both are **code-invisible data classes** the agents *write*
  (`network-builder.md`, `content-repurposer.md` save trackers into the plugin tree).
  They fit Step 15's stated goal ("route the code-invisible data classes so they don't
  silently orphan when the default flips"); the plan named ab-tests/plans as examples,
  not an exhaustive list. As **write-targets** they are self-sufficient — the agent
  creates the file external on first write, no migration entry needed.
- **`config/personas.local.md` deferred — OUT of M0 scope.** Unlike the write-targets,
  this is a **read fallback** in a resolution chain (edition-state → series file →
  plugin `personas.local.md` → template). Repointing the read external without a
  migration dest would break the read, and adding it to `MOVE_FILES` is a `.mjs` change
  Step 15's scope fence forbids. Personas are newsletter/series production data (already
  external via `$LTL_SERIES_ROOT`); the plugin-level `personas.local.md` is a deliberate
  in-plugin fallback library. Left as-is; Step 16's no-bare-path lint targets the
  *migrated* data classes only, so it does not flag personas. Track as a follow-up if a
  full personas externalization is wanted later.

### Step 16 — lint extended 74→81 checks; the no-pin guard caught 5 real survivors

Added to `test-runner.sh`: `EXPECT_REFS` 25→26 + "delta = exactly +1 (data-path-convention.md)"
assert (m3/m11); **Section 13** (no command/agent/skill/hook prose references a migrated
user-data path in-plugin — bare or `${CLAUDE_PLUGIN_ROOT}`-pinned — R1; + the no-`ANALYTICS_ROOT`-
pin sibling M3; + the SC2 `git status --porcelain --ignored` dry-run), each with a
non-vacuity self-test mirroring Section 8; **Section 14** assertion-count anti-erosion
floor (pre-M0 baseline 74, count may only grow — SC6 / assumption 3).

**The no-pin guard earned its keep on first run:** it flagged **5 surviving in-plugin
`ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics"` pins** that Step 11 missed —
`agents/analytics-interpreter.md` (×2) + `skills/linkedin-analytics/SKILL.md` (×3). Step 11
scoped the pin-drop to the *commands* (`import.md`/`report.md`); the agent + skill invoke the
CLI directly and kept the pin, which would force the analytics CLI to read in-plugin even
after the default flipped external (the exact M3 failure). Dropped all 5 (surgical: removed
only the `ANALYTICS_ROOT=… ` prefix, kept the `node --import tsx` form — no CLI-style change),
matching how Step 11 left the commands. Lint now **81/0**.

### Step 18 — release 0.4.0 → 0.5.0 (minor, non-breaking)

Bumped `.claude-plugin/plugin.json` + the three lint-grepped literals (README
`version-0.5.0-blue` badge, CLAUDE.md `(v0.5.0)` header, CHANGELOG `## [0.5.0]`).
Also synced the README `reference_docs-25→26` badge (not lint-checked, but stale after
the convention doc) and refreshed the now-false "user data still lives inside the plugin"
prose in README + CLAUDE.md to "M0 done — data external." Version-consistency lint green;
the only remaining `0.4.0` refs are CHANGELOG history + the `docs/m0/` planning docs that
describe the `0.4.0 → 0.5.0` transition itself (correct, not stale declarations).

**Plan deviation — root marketplace docs NOT touched (scope).** Plan Step 18 listed
`/Users/ktg/.claude/plugins/marketplaces/ktg-plugin-marketplace/{README,CLAUDE}.md` as
"root docs." Skipped: that path is the **installed** marketplace artifact, **outside this
polyrepo**, and STATE.md scope is "KUN denne pluginnen" (+ global "don't write in other
repos"). The binding gate (the lint) checks only this plugin's four declarations, all
green. **Follow-up (separate catalog repo task):** reflect linkedin-studio v0.5.0 in the
marketplace catalog README if/when desired — operator's call, not this plugin's M0.

## Session 3 — Steps 11–13 (2026-06-18)

### Environment reality vs. plan assumptions

The plan was authored assuming the operator's **real `.local.md` runtime data**
sat in the plugin tree (227-line voice profile, analytics exports, draft queue).
On this machine that data is **absent** — it is a clean clone:

- `assets/voice-samples/`: only the PII-free placeholder `authentic-voice-samples.md`
  (+ `.template.md`). No `.local.md` source.
- `assets/drafts/`: only `.gitkeep`. `assets/analytics/`: only `README.md` + empty `ab-tests/`.
- The 4 tracked D2 scaffold instances DO exist (`high-engagement-posts.md`,
  `demographics.md`, `engagement-patterns.md`, `my-post-templates.md`).

**Consequence for Step 12 (live migration):** every `MOVE_FILES` / `MOVE_DIRS`
entry resolved to an absent source → clean no-op. Only the 4 `COPY_FILES`
scaffolds were relocated. Result: `migrated — moved 0, copied 4, skipped 0`;
`.migrated` marker written; idempotent re-run confirmed `already-migrated`.

**Step 12 verify adapted:** the plan's literal check
(`test -f .../voice-samples/authentic-voice-samples.md`) cannot pass without a
`.local.md` source to move, so it was replaced with the achievable post-condition:
`.migrated` marker present + 4 scaffolds external + `migrateData` wired into
`session-start.mjs` + idempotency. The voice MOVE correctly no-op'd. (Operator
pre-approved this adaptation before the run.)

### OUT OF M0 SCOPE — git-history scrub of the leaked post (FOLLOW-UP)

`assets/examples/high-engagement-posts.md` held the operator's **real** LinkedIn
post at HEAD (the "Ralph Wiggum / vibe-coding" post — real names, real engagement
metrics, real personal narrative). Step 13 scrubbed the **working-tree** content
to a generic placeholder (0 `## Post N` sections → personalization score 0, no
PII). **This does not remove the post from git history.** A history rewrite
(`git filter-repo` / BFG) on `assets/examples/high-engagement-posts.md` is a
separate, explicit operation — **deferred, not done here** (brief §13: out of M0
scope). Track until the repo is published.

### Note — external instance retains pre-scrub content (by design)

The Step-12 migration copied `high-engagement-posts.md` to
`~/.claude/linkedin-studio/examples/high-engagement-posts.md` **before** Step 13
scrubbed the in-plugin file (B3 ordering). The external copy therefore still holds
the migrated content. That is the operator's private data dir (outside any repo) —
the operator may curate it; not an M0 concern.

### D2 scaffolds completed

All 6 scaffolds now have a read-only `*-template.*` seed: `case-study-template.md`
and `framework-template.md` already shipped; Step 13 added the 4 missing ones
(`high-engagement-posts-template.md`, `demographics-template.md`,
`engagement-patterns-template.md`, `my-post-templates-template.md`). The 3
already-generic instances seeded their templates verbatim; the high-engagement
template is a freshly-authored generic seed (the old instance was the leak).
