docs(linkedin-studio): M0 per-user data-dir migration brief (ratified)
Grounds the architecture-first M0 workstream (v0.4.0 -> v1.0.0). Three read-only mapping passes corrected the surface from STATE.md's stale 108 to 195 references (~138 in commands/), of which ~186 are .md prose and only ~3 executable seams do real I/O. Six decisions (D1-D6) ratified. Brief only; M0 is not implemented. Next step is /trekplan in a fresh session. Local commit, not pushed (operator-decided). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dd2d666b29
commit
c0e8ff256a
1 changed files with 354 additions and 0 deletions
354
docs/m0/brief.md
Normal file
354
docs/m0/brief.md
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
# M0 — Per-User Data-Dir Migration · Brief
|
||||
|
||||
> **Voyage `/trekbrief`-style task brief.** Foundation document for the **architecture** workstream
|
||||
> on the path `v0.4.0 → v1.0.0`. This brief defines *what M0 is and how we verify it* — it does
|
||||
> **not** implement it. Decisions are surfaced as ratify-by-annotation `DECISION` blocks.
|
||||
>
|
||||
> - **Written:** 2026-06-01
|
||||
> - **Status:** RATIFIED 2026-06-01 — all `DECISION` blocks locked; ready for the `/trekplan` phase (separate session)
|
||||
> - **Workstream:** Architecture (M0). Parallel/blocked workstreams: Hardening (PARKED until M0), Command-testing, GUI.
|
||||
> - **Grounded by:** three read-only mapping passes (access-surface, resolution-seams, on-disk inventory) run 2026-06-01. Counts/paths below are tool-verified, not recalled.
|
||||
|
||||
---
|
||||
|
||||
## 1. Context & motivation
|
||||
|
||||
The hardening workstream (S1–S26) was **paused at S2** when two architecture findings surfaced that
|
||||
no amount of hardening can paper over:
|
||||
|
||||
1. **Grounded simulation must read REAL voice/profile artefacts, not reconstruct them.** During S2 the
|
||||
simulation node verified character counts but ran against a *reconstructed* voice — because the real
|
||||
profile lives in a file (`assets/voice-samples/authentic-voice-samples.local.md`, 7.7 KB) that the
|
||||
command prose does not consistently point at, while `config/user-profile.local.md` **does not exist at
|
||||
all** (only `config/user-profile.template.md` ships).
|
||||
|
||||
2. **User data lives INSIDE the plugin tree.** Per-user content sits under `assets/` and `config/`,
|
||||
defended only by `.gitignore`. That is the wrong location for data that must survive a plugin
|
||||
reinstall/update and must never risk being committed. The **one thing done right** is the state file
|
||||
(`~/.claude/linkedin-studio.local.md`, external via `$HOME`). The seam meant to generalize this —
|
||||
`getAnalyticsRoot()` — is half-built: it honors an `ANALYTICS_ROOT` env override but **defaults to the
|
||||
in-plugin path**, and every caller passes the in-plugin value.
|
||||
|
||||
Hardening on top of an in-plugin data layout would bake the wrong layout into 138+ command instructions.
|
||||
**Architecture first.** M0 relocates user data to a per-user external dir and gives the plugin a single
|
||||
path-resolution seam, so the subsequent hardening, command-testing, and GUI workstreams build on the
|
||||
correct foundation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Problem statement (grounded)
|
||||
|
||||
**The honest numbers** (correcting STATE.md's stale "108"):
|
||||
|
||||
| Surface | In-plugin user-data references |
|
||||
|---|---|
|
||||
| `commands/*.md` (prose instructions) | ~138 |
|
||||
| `agents/*.md` (agent prompts) | 34 |
|
||||
| `hooks/` (prompts + executable `.mjs`) | 15 |
|
||||
| `scripts/` (analytics TS + tests) | 4 |
|
||||
| `skills/*/SKILL.md` | 11 |
|
||||
| `references/` (doc cross-refs) | 4 |
|
||||
| **Total** | **195** |
|
||||
|
||||
**The shape that matters:** ~186 of the 195 are **Markdown prose** — instructions telling Claude *where*
|
||||
to read/write. They are operative (a command that says "read `assets/voice-samples/…`" will read the wrong
|
||||
place after a move) but they are **not code**. Only a small set of **executable seams** actually perform
|
||||
filesystem I/O and hardcode the in-plugin root:
|
||||
|
||||
| Executable seam | What it does | Current root |
|
||||
|---|---|---|
|
||||
| `scripts/analytics/src/utils/storage.ts` → `getAnalyticsRoot()` (`:40-50`) | All analytics read/write | env `ANALYTICS_ROOT` **else** `<pluginRoot>/assets/analytics` |
|
||||
| `hooks/scripts/queue-manager.mjs` (`:11-12`) | Draft queue read/write | env `PLUGIN_ROOT` **else** `join(__dirname,'..','..')` → `assets/drafts/queue.json` |
|
||||
| `hooks/scripts/quick-import.mjs` (`:11-13`) | CSV export staging | `<PLUGIN_ROOT>/assets/analytics/exports` — **no env override** |
|
||||
| `hooks/scripts/personalization-score.mjs` (`:23,34,45,60,75,84,95,106`) | 8 category read paths | `pluginRoot`-relative |
|
||||
| `hooks/scripts/session-start.mjs` (`:408-409`) | `REMEMBER.md` auto-init | **writes inside the plugin tree** (the anti-pattern) |
|
||||
| `hooks/scripts/user-prompt-context.mjs` (`:102`) | Voice-file read for context | `${pluginRoot}/assets/voice-samples/…` |
|
||||
|
||||
**Inconsistencies the migration must resolve, not inherit:**
|
||||
|
||||
- **Voice canonical-file split.** Consumers read the tracked placeholder `authentic-voice-samples.md`
|
||||
(carries `<!-- VOICE_PLACEHOLDER -->`, scores 0), while the *real* profile lives in the gitignored
|
||||
`authentic-voice-samples.local.md`. This is finding 1 above. M0 must declare a single canonical
|
||||
external voice file.
|
||||
- **`config/user-profile.local.md` never exists.** Only the template ships; the personalization scorer
|
||||
reads `config/user-profile.local.md` (`personalization-score.mjs:34`) and therefore always scores that
|
||||
category 0. M0 must define where the profile *instance* lives.
|
||||
- **REMEMBER.md is auto-initialized into the plugin tree** (`session-start.mjs:408`) — the exact anti-pattern
|
||||
M0 exists to fix; a clean before/after exemplar.
|
||||
- **In-place editable scaffolds clobber-risk.** Six personalization categories (case-studies, frameworks,
|
||||
`examples/high-engagement-posts.md`, `audience-insights/demographics.md`, `…/engagement-patterns.md`,
|
||||
`templates/my-post-templates.md`) ship as scaffold files the user edits **in place**, and are **not
|
||||
gitignored** → a plugin update can clobber user edits today.
|
||||
|
||||
---
|
||||
|
||||
## 3. Goal (definition of done)
|
||||
|
||||
**All per-user data lives in a single external data dir that mirrors the state file, resolved through one
|
||||
seam per runtime, with existing data auto-migrated and every graceful-degradation behavior preserved.**
|
||||
|
||||
After M0:
|
||||
- No command, agent, hook, or script reads or writes per-user data inside the plugin tree.
|
||||
- Reinstalling/updating the plugin loses no user data and clobbers no user edits.
|
||||
- The plugin still works on a fresh clone with zero user data (graceful degradation intact).
|
||||
- The grounded-sim (and any consumer) reads the user's **real** voice/profile artefacts — unblocking the
|
||||
hardening workstream's finding 1.
|
||||
|
||||
---
|
||||
|
||||
## 4. Success criteria (testable)
|
||||
|
||||
> Each is a concrete check; the planning phase turns these into commands/tests (global plan-quality rule).
|
||||
|
||||
- **SC1 — Single external root.** A new resolver returns `~/.claude/linkedin-studio/<subdir>` by default
|
||||
(mirroring `~/.claude/linkedin-studio.local.md`), overridable by one documented env var. Verify: unit
|
||||
test asserts default + override for both the `.mjs` and the TS twin.
|
||||
- **SC2 — Zero in-plugin user-data writes.** After running a representative content + analytics flow on a
|
||||
fresh checkout, `git status` shows **no** new/modified files under the plugin tree's `assets/`,
|
||||
`config/*.local.*`, or `REMEMBER.md`; all writes land under the external dir. Verify: scripted dry-run +
|
||||
`git status --porcelain` is empty for user-data paths.
|
||||
- **SC3 — Migration moves the real files.** A migration step relocates the 5 on-disk user files
|
||||
(§6) to the external dir and is **idempotent** + **safe when there is nothing to move**. Verify: run on a
|
||||
fixture with files present → moved; run again → no-op; run on an empty fixture → no error.
|
||||
- **SC4 — Degradation preserved.** All invariants in §8 still hold (voice guardian silent-skip <5 samples,
|
||||
score hidden <3 posts, voice sentinel = 0, analytics empty-data clean-exit, profile-absent = 0 not crash).
|
||||
Verify: the existing degradation tests pass against the external root; new fixtures for the moved paths.
|
||||
- **SC5 — Analytics CLI green externally.** `scripts/analytics` test suite passes with the external default,
|
||||
including the resolver regression lock (`storage-root.test.ts` adapted). Verify: `npm test` green.
|
||||
- **SC6 — Structure lint green.** `scripts/test-runner.sh` stays at Passed/Failed = N/0; any count/path
|
||||
assertions are updated to the new layout. Verify: lint run.
|
||||
- **SC7 — Real-artefact read.** A consumer (voice-guardian / user-prompt-context / a sim) reads the user's
|
||||
actual voice profile from the external dir, not the placeholder. Verify: with a real external voice file
|
||||
present, the injected context contains it; with only the placeholder, degradation applies.
|
||||
|
||||
---
|
||||
|
||||
## 5. Non-goals (scope fence)
|
||||
|
||||
- **NOT hardening.** S1–S26 stay parked. M0 does not re-word command prose for quality, only for path
|
||||
correctness (and even that is a `DECISION` — see D3).
|
||||
- **NOT the GUI.** Separate workstream.
|
||||
- **NOT command-testing** beyond what SC2/SC4/SC7 require to prove the move.
|
||||
- **NOT new features, new commands, new data types.** Pure relocation + seam.
|
||||
- **NOT a schema change.** The analytics JSON schema, queue schema, and state frontmatter are unchanged;
|
||||
only the *root* moves.
|
||||
- **NOT touching already-external data:** the state file and the `$LTL_SERIES_ROOT`/`$HOME/linkedin-series`
|
||||
newsletter production data are already correct and are the destination *pattern*, not move targets.
|
||||
- **NOT migrating plugin-shipped scaffolding** (templates, checklists, fonts, `*-template.*`). Those stay
|
||||
in the tree as read-only seed material.
|
||||
|
||||
---
|
||||
|
||||
## 6. Migration surface (grounded on-disk inventory, 2026-06-01)
|
||||
|
||||
**Files that EXIST and must relocate** (all gitignored today):
|
||||
|
||||
| Current in-plugin path | Size | → external subdir |
|
||||
|---|---|---|
|
||||
| `assets/voice-samples/authentic-voice-samples.local.md` | 7.7 KB | `voice-samples/` |
|
||||
| `assets/drafts/queue.json` | 33 B (empty queue) | `drafts/` |
|
||||
| `assets/analytics/exports/content-2026-W22-seres.csv` | 180 B | `analytics/exports/` |
|
||||
| `assets/analytics/posts/2026-05-26-batch-17.json` | 1,081 B | `analytics/posts/` |
|
||||
| `assets/analytics/weekly-reports/2026-W22.json` | 2,243 B | `analytics/weekly-reports/` |
|
||||
|
||||
**Expected-but-ABSENT** (migration must handle "nothing to move" without error):
|
||||
`config/user-profile.local.md`, `assets/analytics/content-history.md`, `assets/analytics/monthly-reports/`,
|
||||
`assets/drafts/week-*/`.
|
||||
|
||||
**Already-external (do NOT touch — the destination pattern):**
|
||||
`~/.claude/linkedin-studio.local.md` (state) · `$LTL_SERIES_ROOT`/`$HOME/linkedin-series/<slug>/` (newsletter
|
||||
production) · everything routed through `ANALYTICS_ROOT` once the default flips.
|
||||
|
||||
**Stays in-plugin (read-only scaffolding):** all `config/*.template.*`, `assets/templates/*`,
|
||||
`assets/checklists/*`, `assets/**/*-template.md`, `render/fonts/*`, READMEs, `.gitkeep`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Target architecture
|
||||
|
||||
### 7.1 The external root
|
||||
|
||||
```
|
||||
~/.claude/linkedin-studio/ ← new data root (mirrors ~/.claude/linkedin-studio.local.md)
|
||||
voice-samples/ authentic-voice-samples.md (the user's REAL profile)
|
||||
analytics/ exports/ posts/ weekly-reports/ monthly-reports/ ab-tests/ content-history.md
|
||||
drafts/ queue.json week-*/ carousel/ multiplatform/ repurposed/
|
||||
frameworks/ <slug>.md
|
||||
audience-insights/ demographics.md engagement-patterns.md (if D2 = migrate)
|
||||
plans/ <plan>.md
|
||||
profile/ user-profile.md (if D1 = profile-here)
|
||||
```
|
||||
|
||||
Default overridable by **one** env var (proposed `LINKEDIN_STUDIO_DATA`), with back-compat aliases
|
||||
(`ANALYTICS_ROOT`, `STATE_FILE`, `PLUGIN_ROOT`) honored during a deprecation window.
|
||||
|
||||
### 7.2 The resolver (dual-runtime twins — the key constraint)
|
||||
|
||||
Two runtimes cannot share one module file:
|
||||
|
||||
- **Hooks** are `.mjs`, **zero npm deps** (marketplace rule). They use `process.env.HOME || USERPROFILE`.
|
||||
- **Analytics CLI** is **TypeScript** via `tsx`, compiled to `build/`, uses `findPluginRoot` + `import.meta`.
|
||||
|
||||
So M0 introduces **one logical resolver in two byte-equivalent-logic twins**, with a consistency test
|
||||
(the pattern CLAUDE.md already uses for llm-security's "mirrored bit-identical" renderer):
|
||||
|
||||
- `hooks/scripts/data-root.mjs` — `getDataRoot(subdir)`, `getStateFile()`; consumed by all hook scripts.
|
||||
Consolidates the **4× duplicated** `HOME`-string and **~7× duplicated** `PLUGIN_ROOT` derivation.
|
||||
- `scripts/analytics/src/utils/storage.ts` — generalize `getAnalyticsRoot()` → `getDataRoot("analytics")`,
|
||||
keep `ANALYTICS_ROOT` as an alias. The function already separates "find root" from "join subdir," and all
|
||||
storage functions already take `root` as a parameter, so the blast radius of the default-flip is one
|
||||
function body. `findPluginRoot` becomes vestigial *for data* (still used to locate read-only bundled
|
||||
assets like templates).
|
||||
|
||||
### 7.3 Template→instance flow (generalized from the state-file pattern)
|
||||
|
||||
The state file already models the correct flow: **read-only template lives in-plugin
|
||||
(`config/state-file.template.md`), user instance lives external (`~/.claude/`), auto-init copies template →
|
||||
instance on first run** (`session-start.mjs:388-404`, including `mkdirSync(dirname, {recursive:true})`).
|
||||
M0 generalizes this to every data type: ship a template/seed in-plugin, materialize the instance in the
|
||||
external dir on first use, read external-with-fallback-to-template.
|
||||
|
||||
---
|
||||
|
||||
## 8. Graceful-degradation invariants to PRESERVE (file:line)
|
||||
|
||||
These behaviors must hold identically after the move (SC4):
|
||||
|
||||
1. **Voice guardian silent-skip <5 samples** — `hooks/prompts/voice-guardian.md:53` (read at `:31`).
|
||||
2. **Personalization score hidden <3 posts** — `session-start.mjs:192` (status line), `:285` (nudge),
|
||||
keyed on `publishedPostCount` (`:165-170`).
|
||||
3. **Voice sentinel = 0** — `personalization-score.mjs:23-31` (`<!-- VOICE_PLACEHOLDER -->` → 0 pts);
|
||||
profile-absent = 0 not crash (`:34-42`, guarded by `existsSync`).
|
||||
4. **Analytics empty-data clean-exit** — `storage.ts` returns `[]` for missing dirs (`:76-78,147-149,262-264`);
|
||||
`cli.ts:245-247,397-399` exit with a clear message; `ensureDirectories()` (`:55-68`) creates the tree on
|
||||
first write. (The fresh-clone crash fix anchors `getAnalyticsRoot()` on the `.claude-plugin/plugin.json`
|
||||
marker — `:18-50`, commit `798484b`; M0 changes the *default* but keeps the marker for bundled assets.)
|
||||
5. **Voice-profile leak fix intact** — placeholder + `<!-- VOICE_PLACEHOLDER -->` sentinel
|
||||
(`authentic-voice-samples.md:1`) + gitignore (`.gitignore:7,11`) + replace-not-append in both writers
|
||||
(`setup.md:102-114`, `onboarding.md:143-149`). After the move the *external* real file is canonical;
|
||||
the in-plugin placeholder remains only as seed + leak-guard.
|
||||
|
||||
---
|
||||
|
||||
## 9. Decisions (ratify by annotation)
|
||||
|
||||
> Each block: **recommendation** + rationale + alternatives. **All RATIFIED 2026-06-01** — the
|
||||
> recommendation is the locked decision; alternatives are retained as the record of what was considered.
|
||||
> These were the M0 "enige"-points, resolved on the artifact rather than mid-edit.
|
||||
|
||||
### D1 — Where does `user-profile` live? · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** External `profile/user-profile.md`, seeded from `config/user-profile.template.md` on
|
||||
first `setup`. Repoint the scorer (`personalization-score.mjs:34`) and the ~10 prose references.
|
||||
*Rationale:* the `.local.md` instance never exists today and the category always scores 0; externalizing
|
||||
fixes the score AND finding 1 in one move. *Alternatives:* (a) keep the `.local.md` name but external;
|
||||
(b) fold profile into the state file (rejected — different lifecycle, would bloat state).
|
||||
|
||||
### D2 — In-place editable scaffolds (the 6 clobber-risk files) · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** Split each into `*.template.*` (ships, read-only) + external instance; scorer reads
|
||||
external-with-fallback-to-template. *Rationale:* eliminates the update-clobber risk and unifies the model.
|
||||
*Cost:* this is the single biggest scope lever — it adds ~6 template splits + repoints
|
||||
`personalization-score.mjs:45-114` + the corresponding prose. *Alternatives:* (a) **leave in place**
|
||||
(smaller M0, but the clobber-risk and "data in plugin" problem persist for these 6 — deferred, not solved);
|
||||
(b) gitignore-only stopgap (rejected — doesn't fix update-clobber, only commit-leak).
|
||||
**This decision sets whether M0 is "move 5 gitignored files + flip seams" or "+ restructure 6 scaffold/data pairs."**
|
||||
|
||||
### D3 — How are the ~186 prose references repointed? · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** Introduce a documented path convention referenced by a short preamble/variable in each
|
||||
command family, so prose says e.g. `the data dir's voice-samples/` resolved once, minimizing literal
|
||||
per-line edits. Where literal edits are unavoidable, batch them by category. *Rationale:* hand-editing 138
|
||||
command lines is error-prone and noisy; a convention is maintainable. *Alternatives:* (a) literal edit all
|
||||
~186 (highest blast radius, but explicit); (b) a compat **symlink/shim** from old in-plugin paths to the
|
||||
external dir so prose needs no change (rejected as primary — hides the real location, fragile across OSes,
|
||||
violates the "data not in plugin" goal even if symlinked). *Planning phase must prototype the convention on
|
||||
one command family before committing to the count.*
|
||||
|
||||
### D4 — Env var name + back-compat window · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** Primary `LINKEDIN_STUDIO_DATA`; honor `ANALYTICS_ROOT`/`STATE_FILE`/`PLUGIN_ROOT` as
|
||||
deprecated aliases for one minor version, then drop. *Alternative:* reuse `ANALYTICS_ROOT` broadened
|
||||
(rejected — misleading name for non-analytics data).
|
||||
|
||||
### D5 — Version + breaking-ness · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** Land as a **minor** bump with an **automatic, idempotent migration** on session-start
|
||||
(detect in-plugin user files → move → log once), so it is **non-breaking for the user**. *Rationale:* auto-
|
||||
migration + back-compat aliases means no manual user action. *Alternative:* tag breaking + require a manual
|
||||
`/linkedin:migrate` (rejected unless auto-migration proves unsafe — adds friction for a solo user-base).
|
||||
|
||||
### D6 — Canonical voice file name post-move · `RATIFIED 2026-06-01`
|
||||
**Recommendation:** External `voice-samples/authentic-voice-samples.md` is canonical (drop the `.local`
|
||||
suffix — external location already provides the gitignore-equivalent safety). In-plugin placeholder keeps
|
||||
its name + sentinel as seed/leak-guard. *Alternative:* keep `.local.md` externally (harmless, but `.local`
|
||||
is a gitignore convention that's meaningless outside the repo).
|
||||
|
||||
---
|
||||
|
||||
## 10. Risks
|
||||
|
||||
- **R1 — Prose/code drift.** If code seams move but prose references don't (or vice-versa), commands write
|
||||
to the new dir while reading the old (or Claude follows stale prose). *Mitigation:* SC2 + SC7 as gates;
|
||||
D3 convention; lint assertion that no command prose references a bare in-plugin user-data path.
|
||||
- **R2 — Twin divergence.** The `.mjs` and TS resolvers drift apart. *Mitigation:* a consistency test
|
||||
asserting identical default + override semantics (model: `storage-root.test.ts`).
|
||||
- **R3 — Migration data loss.** A move that isn't idempotent/atomic could lose the 7.7 KB voice file.
|
||||
*Mitigation:* copy-then-verify-then-remove; idempotency test (SC3); never delete source until destination
|
||||
confirmed.
|
||||
- **R4 — Degradation regression.** Repointing read paths breaks a silent-skip into a crash. *Mitigation:*
|
||||
SC4 runs the existing degradation tests against the new root + new fixtures.
|
||||
- **R5 — Scope creep into hardening.** Touching 138 command files invites "while I'm here" edits.
|
||||
*Mitigation:* Non-goals §5; path-correctness edits ONLY; hardening stays parked.
|
||||
- **R6 — Cross-platform HOME.** `$HOME` vs `%USERPROFILE%`. *Mitigation:* reuse the existing
|
||||
`process.env.HOME || process.env.USERPROFILE` idiom already proven in 4 hook scripts.
|
||||
|
||||
---
|
||||
|
||||
## 11. Open questions for the planning phase
|
||||
|
||||
- Does any consumer read analytics outside `getAnalyticsRoot()` (the agents found `ab-tests/` referenced
|
||||
only in `ab-test.md` prose, not routed through the resolver)? Enumerate before flipping the default.
|
||||
- Can the D3 convention actually reduce edits, or do commands need literal paths for Claude to act on?
|
||||
Prototype on one family (e.g. the voice readers) and measure.
|
||||
- Exact deprecation window for D4 aliases — tie to a named version.
|
||||
- Should `content-history.md` (runtime user data, template-seeded) follow analytics or drafts?
|
||||
|
||||
---
|
||||
|
||||
## 12. Verification plan
|
||||
|
||||
> Concrete checks proving M0 is correct (global plan-quality rule — testable, not "see if it works").
|
||||
|
||||
1. **Resolver unit tests** (`.mjs` + TS): default returns `~/.claude/linkedin-studio/<subdir>`; env override
|
||||
wins; twins agree. → SC1, R2.
|
||||
2. **Fresh-clone flow:** on a clean checkout with no user data, run a content command + an analytics import +
|
||||
a report; then `git status --porcelain` filtered to user-data paths must be **empty**, and the files must
|
||||
exist under the external dir. → SC2.
|
||||
3. **Migration fixtures:** (a) files-present → moved + source gone + destination verified; (b) re-run → no-op;
|
||||
(c) empty → no error. → SC3, R3.
|
||||
4. **Degradation suite:** voice-guardian with 0/4/5 samples; scorer with absent profile, placeholder voice,
|
||||
absent analytics; CLI on empty data exits cleanly. → SC4, R4.
|
||||
5. **Analytics suite green** with external default incl. adapted `storage-root.test.ts`. → SC5.
|
||||
6. **`scripts/test-runner.sh`** Passed/Failed = N/0 with updated count/path assertions + a new assertion that
|
||||
no command prose references a bare in-plugin user-data path. → SC6, R1.
|
||||
7. **Real-artefact read:** external real voice file present → appears in injected context; only placeholder →
|
||||
degradation. → SC7, finding 1 closed.
|
||||
|
||||
---
|
||||
|
||||
## 13. References (tool-verified anchors)
|
||||
|
||||
- `scripts/analytics/src/utils/storage.ts:40-50` — `getAnalyticsRoot()` (the seam to generalize)
|
||||
- `scripts/analytics/tests/storage-root.test.ts` — resolver regression lock (the test template)
|
||||
- `scripts/analytics/src/cli.ts:444` — sole `getAnalyticsRoot()` caller; `:245-247,397-399` empty-data exits
|
||||
- `hooks/scripts/state-updater.mjs:11-12` — `HOME` + `STATE_FILE` seam (correct external pattern)
|
||||
- `hooks/scripts/session-start.mjs:14-15` (HOME), `:388-404` (auto-init-from-template), `:408-409` (REMEMBER anti-pattern), `:165-170,190-192,285` (score gating)
|
||||
- `hooks/scripts/queue-manager.mjs:11-12` — in-plugin `QUEUE_FILE` (M0 target)
|
||||
- `hooks/scripts/quick-import.mjs:11-13` — in-plugin `EXPORTS_DIR`, no env override (M0 target)
|
||||
- `hooks/scripts/personalization-score.mjs:23-114` — 8 category read paths (+sentinel/profile degradation)
|
||||
- `hooks/scripts/user-prompt-context.mjs:102-105` — voice-file read, existence-guarded
|
||||
- `hooks/prompts/voice-guardian.md:31,53` — voice read + <5-sample silent-skip
|
||||
- `config/state-file.template.md:4` — self-documents the template→external split
|
||||
- `config/edition-state.template.json:4` / `commands/newsletter.md:46,148` — `$LTL_SERIES_ROOT` external production data (already correct)
|
||||
- `.gitignore:7,11,37,38,41-44` — near-complete manifest of the user-data classes M0 relocates
|
||||
|
||||
---
|
||||
|
||||
_Counts and paths in this brief were verified by direct codebase mapping on 2026-06-01, not recalled.
|
||||
The "108" figure in STATE.md is superseded by the grounded ~138 (commands) / 195 (total)._
|
||||
Loading…
Add table
Add a link
Reference in a new issue