fix(linkedin-studio): ship placeholder voice profile, gitignore real, sentinel detection
Wave 2 / Step 5 of the remediation plan (coupled criticals: voice-leak + placeholder-detection). Voice profile (the adopter-default leak): - Ship a PII-free placeholder at authentic-voice-samples.md carrying a <!-- VOICE_PLACEHOLDER --> sentinel + neutral default voice principles. - Migrate the author's real profile to gitignored authentic-voice-samples.local.md (already matched by *.local.md; added an explicit, commented .gitignore entry so the intent is unmissable). NO git-history rewrite — the historical file is attributed authorship, not a secret (per the plan threat model). - Add authentic-voice-samples.template.md — a clean fill-in template for adopters. - personalization-score.mjs: detect the sentinel (deterministic) instead of the unreliable `[Your Name]` heuristic, so the placeholder scores 0 voice points and a populated profile (sentinel removed) earns the 25. - Both voice writers replace-not-append on the placeholder: setup.md (merge -> replace-if-placeholder) and onboarding.md (append -> replace-if-placeholder), so populating removes the sentinel; updated setup.md's stale heuristic table. Operator decisions (deviations from plan-literal, approved this session): - KEEP the plugin.json author name. The plan said scrub author -> neutral/org, but that contradicts its own LICENSE reasoning (intentional MIT attribution) and all 5 sibling plugins keep author = the author; scrubbing only this one would create inconsistency for zero security gain (the name is public-by-design). The voice placeholder fully fixes the adopter-inheritance bug. - Scrub the stale "January 2026 360Brew" brand from the plugin.json description and the "360brew" keyword (locked decision: no publishable model name/date). This is a Wave-1 propagation miss surfaced here because plugin.json was in Step 5's touch-scope. Flagged for follow-up (NOT done here — out of Session 2 scope): - The lint's stat-consistency grep (scripts/test-runner.sh) scans references/, commands/, skills/, hooks/prompts/, CLAUDE.md, README.md — but NOT .claude-plugin/plugin.json, which is why the 360Brew brand slipped Wave 1. Needs a Session-1-scoped lint extension to add plugin.json to the scan set. - Readers (user-prompt-context.mjs, voice-guardian.md, state-update-reminder.md) read the tracked .md (placeholder), per the plan. The operator's real voice now lives in the gitignored .local.md, which nothing reads. To use it, readers + the voice score should prefer .local.md (matching the user-profile.local.md precedent). Deferred as a coherence follow-up for operator review. Test-first: hooks/scripts/__tests__/personalization-score.test.mjs (red on the placeholder scoring 25 under the old heuristic, green after the sentinel fix). Hook suite 62/62, structural lint 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
798484bf0c
commit
911871ff53
8 changed files with 198 additions and 209 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "linkedin-studio",
|
"name": "linkedin-studio",
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the January 2026 360Brew algorithm baked in. v3.1.0 (Endring 9) adds a cold adversarial review package to `/linkedin:newsletter` — Step 6.5 + the standalone `/linkedin:headless-review` command run three new headless archetypes (content-reviewer, language-reviewer, fact-reviewer) plus the persona reviewer with NO drafting-session context — a `/linkedin:pivot` command that re-opens cleared gates after a late change, and per-artifact personas (one or more readers configurable per edition). v3.0.0 renamed the plugin (was `linkedin-thought-leadership`): slug, agent namespace, and state-file path are `linkedin-studio`; the `/linkedin:*` commands are unchanged.",
|
"description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the 2026 relevance-ranking model baked in. v3.1.0 (Endring 9) adds a cold adversarial review package to `/linkedin:newsletter` — Step 6.5 + the standalone `/linkedin:headless-review` command run three new headless archetypes (content-reviewer, language-reviewer, fact-reviewer) plus the persona reviewer with NO drafting-session context — a `/linkedin:pivot` command that re-opens cleared gates after a late change, and per-artifact personas (one or more readers configurable per edition). v3.0.0 renamed the plugin (was `linkedin-thought-leadership`): slug, agent namespace, and state-file path are `linkedin-studio`; the `/linkedin:*` commands are unchanged.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Kjell Tore Guttormsen"
|
"name": "Kjell Tore Guttormsen"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace",
|
"repository": "https://git.fromaitochitta.com/open/ktg-plugin-marketplace",
|
||||||
"keywords": ["linkedin", "content-creation", "newsletter", "analytics", "360brew"]
|
"keywords": ["linkedin", "content-creation", "newsletter", "analytics", "relevance-ranking"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
plugins/linkedin-studio/.gitignore
vendored
4
plugins/linkedin-studio/.gitignore
vendored
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
# Local configuration
|
# Local configuration
|
||||||
*.local.md
|
*.local.md
|
||||||
|
# Real voice profile is personal data — adopters keep theirs local; the tracked
|
||||||
|
# authentic-voice-samples.md ships as a sentinel placeholder. (Already matched by
|
||||||
|
# *.local.md above; listed explicitly so the intent is unmissable.)
|
||||||
|
assets/voice-samples/authentic-voice-samples.local.md
|
||||||
|
|
||||||
# Session state (personal activity, auto-initialized from template)
|
# Session state (personal activity, auto-initialized from template)
|
||||||
REMEMBER.md
|
REMEMBER.md
|
||||||
|
|
|
||||||
|
|
@ -1,227 +1,70 @@
|
||||||
# Authentic Voice Samples - Kjell Tore Guttormsen
|
<!-- VOICE_PLACEHOLDER -->
|
||||||
|
<!--
|
||||||
|
This is the SHIPPED PLACEHOLDER voice profile — neutral defaults, not anyone's
|
||||||
|
personal voice. The VOICE_PLACEHOLDER sentinel above keeps your voice
|
||||||
|
personalization score at 0 until you replace this file with your own profile.
|
||||||
|
|
||||||
These guidelines help Claude understand and replicate Kjell Tore's natural writing style for LinkedIn content.
|
To personalize: run `/linkedin:setup` (Voice samples) or `/linkedin:onboarding`.
|
||||||
|
Those workflows overwrite this file with a profile built from your own samples.
|
||||||
|
Prefer to start from a clean form? Copy `authentic-voice-samples.template.md`.
|
||||||
|
|
||||||
## Voice Profile Summary
|
If you want to keep your real profile out of version control, save it as
|
||||||
|
`authentic-voice-samples.local.md` (gitignored) instead of editing this file.
|
||||||
|
-->
|
||||||
|
|
||||||
Kjell Tore does not have traditional writing samples to share. Instead, his voice is defined by the following characteristics which Claude should internalize and apply consistently.
|
# Authentic Voice Samples — Placeholder (neutral defaults)
|
||||||
|
|
||||||
---
|
These are neutral, widely-applicable defaults so the plugin produces reasonable
|
||||||
|
content before you personalize. Replace them with your own voice via
|
||||||
|
`/linkedin:setup`. Until you do, the voice category scores 0.
|
||||||
|
|
||||||
## Core Voice Characteristics
|
## Core Voice Characteristics
|
||||||
|
|
||||||
### 1. Solution-Oriented Mindset
|
1. **Solution-oriented** — frame problems with a path forward, not just complaints.
|
||||||
- Every problem is presented as an opportunity
|
2. **Factually grounded** — base claims on evidence; acknowledge uncertainty openly.
|
||||||
- Never complains without offering a path forward
|
3. **Non-judgmental** — explain without criticizing people, companies, or decisions.
|
||||||
- Focuses on "what can be done" rather than "what went wrong"
|
4. **Curious and open** — treat "I don't know" as a starting point, not a weakness.
|
||||||
- Sees challenges as interesting puzzles to solve
|
5. **Story-led** — open with a concrete example before the abstract point.
|
||||||
|
6. **Actionable** — end with something the reader can do, or a clear takeaway.
|
||||||
|
|
||||||
### 2. Factual Grounding
|
## Do's
|
||||||
- Statements are based on facts, not assumptions
|
|
||||||
- If uncertain, acknowledges uncertainty openly
|
|
||||||
- Prefers data and evidence over opinions
|
|
||||||
- Avoids speculation presented as fact
|
|
||||||
|
|
||||||
### 3. Non-Judgmental Tone
|
- ✅ Open with a story or concrete example before the concept.
|
||||||
- Observes and explains without criticizing others
|
- ✅ Use clear, accessible language even for technical topics.
|
||||||
- Builds up, never tears down
|
- ✅ Explain jargon on first use — assume intelligence, not prior knowledge.
|
||||||
- Avoids negative commentary about people, companies, or decisions
|
- ✅ Show rather than tell.
|
||||||
- When discussing alternatives, frames as "different approaches" not "better/worse"
|
- ✅ End with a specific, actionable takeaway.
|
||||||
|
- ✅ Keep standard posts concise (≈800–1500 characters).
|
||||||
|
|
||||||
### 4. Curiosity and Openness
|
## Don'ts
|
||||||
- Genuinely interested in learning new things
|
|
||||||
- Open to new ideas and approaches
|
|
||||||
- Asks questions to understand, not to challenge
|
|
||||||
- Embraces "I don't know" as a starting point for exploration
|
|
||||||
|
|
||||||
### 5. Storytelling Approach
|
- ❌ Corporate buzzwords ("game-changer", "leverage", "synergy", "disrupt").
|
||||||
- Uses narrative techniques to make points memorable
|
- ❌ Criticizing people, companies, or decisions.
|
||||||
- Varies storytelling patterns based on content:
|
- ❌ Claims without evidence.
|
||||||
- Hero's journey (transformation stories)
|
- ❌ More than 1–2 emojis per post.
|
||||||
- Problem-solution (practical content)
|
- ❌ Generic motivational filler or preachy lecturing.
|
||||||
- Before-after (showing change/improvement)
|
|
||||||
- Discovery narrative (learning something new)
|
|
||||||
- Day-in-the-life (practical application)
|
|
||||||
- Shows rather than tells
|
|
||||||
|
|
||||||
### 6. Actionable Conclusions
|
|
||||||
- Ends with something the reader can do
|
|
||||||
- The more actionable, the better
|
|
||||||
- If no clear action, provides a clear summary/takeaway
|
|
||||||
- Never ends on a vague note
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-Sample Analysis
|
|
||||||
|
|
||||||
### Do's (Things that sound like Kjell Tore)
|
|
||||||
|
|
||||||
- ✅ Start with stories or concrete examples before explaining concepts
|
|
||||||
- ✅ Use clear, accessible language even for technical topics
|
|
||||||
- ✅ Explain technical concepts thoroughly - assume intelligence, not knowledge
|
|
||||||
- ✅ Show rather than tell - demonstrate with examples
|
|
||||||
- ✅ End with actionable takeaways - what can the reader do NOW?
|
|
||||||
- ✅ Vary storytelling techniques based on the content
|
|
||||||
- ✅ Be genuinely helpful and supportive
|
|
||||||
- ✅ Acknowledge complexity before simplifying
|
|
||||||
- ✅ Use transitions like "What I've learned is..." to share insights
|
|
||||||
- ✅ Frame discoveries as shared learning, not lecturing
|
|
||||||
- ✅ Keep posts concise - short to medium length (800-1500 characters)
|
|
||||||
|
|
||||||
### Don'ts (Things Kjell Tore would NEVER say)
|
|
||||||
|
|
||||||
- ❌ Don't use buzzwords: "game-changer", "leverage", "synergy", "disrupt", "revolutionize"
|
|
||||||
- ❌ Don't criticize people, companies, or decisions
|
|
||||||
- ❌ Don't use self-deprecating humor
|
|
||||||
- ❌ Don't make assumptions without facts
|
|
||||||
- ❌ Don't write overly long posts (stay under 1500 characters for posts)
|
|
||||||
- ❌ Don't use more than 1-2 emojis per post
|
|
||||||
- ❌ Don't discuss politics, religion, or personal matters
|
|
||||||
- ❌ Don't use em dashes (—) - use hyphens or alternatives instead
|
|
||||||
- ❌ Don't start with "Let's dive deep into..."
|
|
||||||
- ❌ Don't use excessive exclamation marks!!!
|
|
||||||
- ❌ Don't use generic motivational phrases
|
|
||||||
- ❌ Don't be preachy or lecture the reader
|
|
||||||
- ❌ Don't use "we" when you mean "I" (be direct about personal experience)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Signature Phrases
|
## Signature Phrases
|
||||||
|
|
||||||
Use these naturally when appropriate - don't force them:
|
_(Add the phrases you naturally use once you personalize this profile.)_
|
||||||
|
|
||||||
- "Let me show you..."
|
|
||||||
- "What I've learned is..."
|
|
||||||
- "Here is the secret to..."
|
|
||||||
|
|
||||||
These phrases signal a transition to insight or demonstration. Use them to introduce key points or revelations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Vocabulary Preferences
|
## Vocabulary Preferences
|
||||||
|
|
||||||
### Technical Terms - How to Handle
|
_(List the terms you always explain, and the words/phrases you avoid.)_
|
||||||
|
|
||||||
- **RAG (Retrieval-Augmented Generation):** Always explain on first use
|
|
||||||
- **MCP (Model Context Protocol):** Explain what it enables, not just the acronym
|
|
||||||
- **Copilot Studio:** Can assume some familiarity with Microsoft ecosystem
|
|
||||||
- **Skills (Claude):** Explain as "reusable instruction sets" or similar
|
|
||||||
- **Low-code:** Generally understood, but clarify scope if needed
|
|
||||||
|
|
||||||
**Principle:** Assume intelligence, not knowledge. Explain jargon without being condescending.
|
|
||||||
|
|
||||||
### Words/Phrases to AVOID
|
|
||||||
|
|
||||||
- "Game-changer"
|
|
||||||
- "Revolutionary"
|
|
||||||
- "Disruption" / "Disruptive"
|
|
||||||
- "Leverage" (as a verb)
|
|
||||||
- "Synergy"
|
|
||||||
- "Deep dive" / "Let's dive deep"
|
|
||||||
- "Unpack" (as in "let me unpack this")
|
|
||||||
- "At the end of the day"
|
|
||||||
- "It is what it is"
|
|
||||||
- "Touch base"
|
|
||||||
- "Circle back"
|
|
||||||
- "Low-hanging fruit"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Humor and Personality
|
|
||||||
|
|
||||||
- **Humor style:** Mostly absent in professional content. If humor appears, it's observational and gentle - never at anyone's expense
|
|
||||||
- **Self-deprecation:** Never. Don't undermine your own credibility.
|
|
||||||
- **Cultural references:** Avoid pop culture references. Stick to professional/work context.
|
|
||||||
- **Analogies:** Use when helpful for explanation. Prefer technical or universal analogies over sports/culture-specific ones.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Transitions and Flow
|
|
||||||
|
|
||||||
### How to Move Between Ideas
|
|
||||||
|
|
||||||
- Use questions: "So what does this mean for..."
|
|
||||||
- Use signposting: "Three things matter here..."
|
|
||||||
- Use revelation: "Here's what I discovered..."
|
|
||||||
- Use contrast: "The common approach is X. But what actually works is Y."
|
|
||||||
|
|
||||||
### How to Conclude
|
|
||||||
|
|
||||||
- Always tie back to practical implications
|
|
||||||
- End with a specific action the reader can take
|
|
||||||
- If no action possible, summarize the key insight clearly
|
|
||||||
- Occasionally invite discussion, but don't overuse "What do you think?" as a crutch
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technical Depth Adaptation
|
|
||||||
|
|
||||||
Match technical depth to the target audience:
|
|
||||||
|
|
||||||
### For Leaders
|
|
||||||
- High-level concepts
|
|
||||||
- Business implications
|
|
||||||
- Strategic decisions
|
|
||||||
- ROI and outcomes
|
|
||||||
- Avoid implementation details
|
|
||||||
|
|
||||||
### For Low-Code Developers
|
|
||||||
- Practical tips and patterns
|
|
||||||
- Step-by-step guidance
|
|
||||||
- Tool-specific insights
|
|
||||||
- Common pitfalls and solutions
|
|
||||||
- Can include some technical detail
|
|
||||||
|
|
||||||
### For AI Architects
|
|
||||||
- Technical depth welcome
|
|
||||||
- Architecture patterns
|
|
||||||
- Integration approaches
|
|
||||||
- Trade-offs and decisions
|
|
||||||
- Code snippets when relevant
|
|
||||||
|
|
||||||
### For Power Users
|
|
||||||
- Productivity gains
|
|
||||||
- Workflow improvements
|
|
||||||
- Tool comparisons
|
|
||||||
- Time-saving techniques
|
|
||||||
- Practical shortcuts
|
|
||||||
|
|
||||||
**Key principle:** Always ensure technical content is well-explained and followable, regardless of depth. If you go technical, go all the way - don't half-explain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Language Guidelines
|
## Language Guidelines
|
||||||
|
|
||||||
- **Always English** for all LinkedIn content
|
- Write in one consistent language per post; keep it accessible to non-native readers.
|
||||||
- Clear, international English accessible to non-native speakers
|
- Prefer simple sentence structures for complex ideas.
|
||||||
- Avoid idioms that don't translate well internationally
|
|
||||||
- Prefer simple sentence structures for complex ideas
|
|
||||||
- Never use em dashes (—) - use hyphens, commas, or separate sentences instead
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Instructions for Claude
|
## Instructions for Claude
|
||||||
|
|
||||||
When generating LinkedIn content for Kjell Tore:
|
This is a placeholder. When it is in place (sentinel present), treat the defaults
|
||||||
|
above as a reasonable baseline, and prompt the user to personalize via
|
||||||
1. **Start with his voice profile** (from this document)
|
`/linkedin:setup`. Once personalized, these instructions are replaced by the
|
||||||
2. **Check the content pillar** - which audience is this for?
|
user's own profile.
|
||||||
3. **Choose appropriate storytelling technique** for the content type
|
|
||||||
4. **Ensure actionable conclusion** - what can the reader DO?
|
|
||||||
5. **Verify against Don'ts list** - no buzzwords, no criticism, no assumptions
|
|
||||||
6. **Keep length in check** - 800-1500 characters for posts
|
|
||||||
|
|
||||||
**Priority:** Sound like Kjell Tore > Optimize for algorithm
|
|
||||||
|
|
||||||
**Exception:** If a phrase or approach would harm reach (external links, engagement bait), flag it but maintain his voice in everything else.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Update Log
|
|
||||||
|
|
||||||
- 2025-11-30: Initial voice profile created based on interview
|
|
||||||
|
|
||||||
## Collected Post Samples
|
## Collected Post Samples
|
||||||
|
|
||||||
<!-- Posts are saved here automatically by the Stop hook after each session where content is created. -->
|
<!-- Posts are appended here automatically by the Stop hook after sessions where content is created. -->
|
||||||
<!-- The voice-trainer agent uses these for 6-dimension drift scoring. Needs 5+ samples for reliable results. -->
|
<!-- The voice-trainer agent uses these for 6-dimension drift scoring. Needs 5+ samples for reliable results. -->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Authentic Voice Samples — Template
|
||||||
|
|
||||||
|
Fill this in with YOUR voice, then save it as `authentic-voice-samples.md`
|
||||||
|
(or, to keep it out of version control, `authentic-voice-samples.local.md`).
|
||||||
|
`/linkedin:setup` can build this for you from 3–5 of your own posts.
|
||||||
|
|
||||||
|
Delete every `[bracketed]` prompt as you replace it. Do NOT leave the
|
||||||
|
`<!-- VOICE_PLACEHOLDER -->` sentinel anywhere in your finished profile — its
|
||||||
|
presence keeps the voice personalization score at 0.
|
||||||
|
|
||||||
|
## Core Voice Characteristics
|
||||||
|
|
||||||
|
[List 4–6 traits that define how you write. For each: a one-line description and,
|
||||||
|
where useful, the patterns you reach for. Examples: solution-oriented, factually
|
||||||
|
grounded, story-led, non-judgmental, actionable.]
|
||||||
|
|
||||||
|
## Do's
|
||||||
|
|
||||||
|
- ✅ [Things that sound like you — openings, structure, language level, how you close.]
|
||||||
|
|
||||||
|
## Don'ts
|
||||||
|
|
||||||
|
- ❌ [Words, tones, and moves you avoid — buzzwords, em dashes, over-long posts, etc.]
|
||||||
|
|
||||||
|
## Signature Phrases
|
||||||
|
|
||||||
|
[The phrases you genuinely use to transition into insight or demonstration.
|
||||||
|
Keep the list short and real — forced catchphrases read as fake.]
|
||||||
|
|
||||||
|
## Vocabulary Preferences
|
||||||
|
|
||||||
|
### Terms to always explain on first use
|
||||||
|
[Domain acronyms/terms your audience may not know.]
|
||||||
|
|
||||||
|
### Words/phrases to AVOID
|
||||||
|
[Your personal no-go list.]
|
||||||
|
|
||||||
|
## Language Guidelines
|
||||||
|
|
||||||
|
- [Which language(s) you publish in, and any accessibility rules — e.g. simple
|
||||||
|
sentences for complex ideas, avoid hard-to-translate idioms.]
|
||||||
|
|
||||||
|
## Technical Depth Adaptation
|
||||||
|
|
||||||
|
[If you write for multiple audiences, note how depth shifts per audience
|
||||||
|
(leaders vs practitioners vs power users).]
|
||||||
|
|
||||||
|
## Instructions for Claude
|
||||||
|
|
||||||
|
When generating content in this voice:
|
||||||
|
1. Start from this profile.
|
||||||
|
2. [Your priority order — e.g. "sound like me > optimize for algorithm".]
|
||||||
|
3. Verify against the Don'ts list before finishing.
|
||||||
|
|
||||||
|
## Update Log
|
||||||
|
|
||||||
|
- [YYYY-MM-DD]: Initial voice profile created.
|
||||||
|
|
||||||
|
## Collected Post Samples
|
||||||
|
|
||||||
|
<!-- Posts are appended here automatically by the Stop hook after sessions where content is created. -->
|
||||||
|
|
@ -139,7 +139,13 @@ Use AskUserQuestion:
|
||||||
4. "Paste a paragraph you've written that sounds like YOU (email, doc, anything)"
|
4. "Paste a paragraph you've written that sounds like YOU (email, doc, anything)"
|
||||||
5. "Any words or phrases you'd NEVER use?"
|
5. "Any words or phrases you'd NEVER use?"
|
||||||
|
|
||||||
Save responses to `assets/voice-samples/authentic-voice-samples.md` under a new section `## Quick Voice Interview` (append, don't overwrite existing content).
|
Save the responses to `assets/voice-samples/authentic-voice-samples.md`. **If the
|
||||||
|
file is the shipped placeholder** (it contains `<!-- VOICE_PLACEHOLDER -->`),
|
||||||
|
**REPLACE it entirely** with the profile built from the answers — the
|
||||||
|
`<!-- VOICE_PLACEHOLDER -->` sentinel must NOT remain, or the voice score stays at
|
||||||
|
0 after the user fills it in. **If the file is already a populated profile**, add a
|
||||||
|
`## Quick Voice Interview` section instead of overwriting. Either way, the final
|
||||||
|
file must contain no `<!-- VOICE_PLACEHOLDER -->`.
|
||||||
|
|
||||||
**If user profile selected:** Ask for:
|
**If user profile selected:** Ask for:
|
||||||
1. Full name
|
1. Full name
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Read these 8 asset files and detect placeholder patterns to calculate the curren
|
||||||
|
|
||||||
| Category | Weight | File/Directory | Placeholder Detection |
|
| Category | Weight | File/Directory | Placeholder Detection |
|
||||||
|----------|--------|----------------|----------------------|
|
|----------|--------|----------------|----------------------|
|
||||||
| Voice samples | 25 | `assets/voice-samples/authentic-voice-samples.md` | Check for `[Your Name]` or if file has <50 lines |
|
| Voice samples | 25 | `assets/voice-samples/authentic-voice-samples.md` | Placeholder if it contains the `<!-- VOICE_PLACEHOLDER -->` sentinel (or has <50 lines) |
|
||||||
| User profile | 20 | `config/user-profile.local.md` | Check if file exists; count `[Your ` placeholders |
|
| User profile | 20 | `config/user-profile.local.md` | Check if file exists; count `[Your ` placeholders |
|
||||||
| Case studies | 15 | `assets/case-studies/*.md` | Count non-template `.md` files (exclude `case-study-template.md`) |
|
| Case studies | 15 | `assets/case-studies/*.md` | Count non-template `.md` files (exclude `case-study-template.md`) |
|
||||||
| Frameworks | 10 | `assets/frameworks/*.md` | Count non-template `.md` files (exclude `framework-template.md`) |
|
| Frameworks | 10 | `assets/frameworks/*.md` | Count non-template `.md` files (exclude `framework-template.md`) |
|
||||||
|
|
@ -96,14 +96,19 @@ Based on their answer, run the corresponding sub-workflow below.
|
||||||
- How they handle technical depth
|
- How they handle technical depth
|
||||||
- How they conclude (CTA style, takeaway style)
|
- How they conclude (CTA style, takeaway style)
|
||||||
4. Read the existing `assets/voice-samples/authentic-voice-samples.md`
|
4. Read the existing `assets/voice-samples/authentic-voice-samples.md`
|
||||||
5. **Merge** new findings with existing content (don't overwrite existing data):
|
5. **If the file is the shipped placeholder** (it contains `<!-- VOICE_PLACEHOLDER -->`):
|
||||||
|
**REPLACE it entirely** with the profile built from the user's samples. The
|
||||||
|
placeholder's `<!-- VOICE_PLACEHOLDER -->` sentinel must NOT survive — if it
|
||||||
|
does, the voice category stays at 0 even after the user fills in real data.
|
||||||
|
**Otherwise** (the file is already a populated profile), **merge** new findings
|
||||||
|
into the existing content (don't discard existing data):
|
||||||
- Update "Core Voice Characteristics" if new patterns found
|
- Update "Core Voice Characteristics" if new patterns found
|
||||||
- Add new entries to "Do's" and "Don'ts" lists
|
- Add new entries to "Do's" and "Don'ts" lists
|
||||||
- Update "Signature Phrases" with newly detected phrases
|
- Update "Signature Phrases" with newly detected phrases
|
||||||
- Add "Vocabulary Preferences" based on word analysis
|
- Add "Vocabulary Preferences" based on word analysis
|
||||||
- Update "Update Log" with today's date
|
- Update "Update Log" with today's date
|
||||||
|
|
||||||
6. Write the updated file back.
|
6. Write the file back, and confirm it contains no `<!-- VOICE_PLACEHOLDER -->`.
|
||||||
|
|
||||||
**Important:** Ask "Would you like to paste more samples?" after analyzing the first batch. More samples = better voice model.
|
**Important:** Ask "Would you like to paste more samples?" after analyzing the first batch. More samples = better voice model.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { describe, test, afterEach } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { calculateScore } from '../personalization-score.mjs';
|
||||||
|
|
||||||
|
// Step 5 (remediation): the shipped voice profile is a PII-free placeholder
|
||||||
|
// carrying the sentinel below. Detection must key on the sentinel — NOT the old
|
||||||
|
// `[Your Name]` heuristic — so the placeholder earns 0 of the 25 voice points,
|
||||||
|
// and a populated profile (sentinel removed) earns them.
|
||||||
|
const SENTINEL = '<!-- VOICE_PLACEHOLDER -->';
|
||||||
|
|
||||||
|
// A >50-line body that contains NEITHER the sentinel NOR `[Your Name]`, so the
|
||||||
|
// only thing distinguishing placeholder from real profile is the sentinel.
|
||||||
|
const LONG_BODY = Array.from({ length: 60 }, (_, i) => `- voice characteristic line ${i + 1}`).join('\n');
|
||||||
|
|
||||||
|
function makePluginRoot() {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'lis-personalization-'));
|
||||||
|
mkdirSync(join(root, 'assets', 'voice-samples'), { recursive: true });
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeVoice(root, content) {
|
||||||
|
writeFileSync(join(root, 'assets', 'voice-samples', 'authentic-voice-samples.md'), content, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('calculateScore — voice samples (sentinel-based placeholder detection)', () => {
|
||||||
|
let root;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (root && existsSync(root)) {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
root = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('placeholder with the sentinel scores 0 voice points (even >50 lines, no [Your Name])', () => {
|
||||||
|
root = makePluginRoot();
|
||||||
|
writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n\n${LONG_BODY}\n`);
|
||||||
|
|
||||||
|
const { score, personalized } = calculateScore(root);
|
||||||
|
|
||||||
|
assert.equal(score, 0, 'placeholder must not earn the 25 voice points');
|
||||||
|
assert.equal(personalized, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a real profile (no sentinel, >50 lines) earns the 25 voice points', () => {
|
||||||
|
root = makePluginRoot();
|
||||||
|
writeVoice(root, `# My Voice Profile\n\n${LONG_BODY}\n`);
|
||||||
|
|
||||||
|
const { score, personalized } = calculateScore(root);
|
||||||
|
|
||||||
|
assert.equal(score, 25, 'a populated profile must earn the 25 voice points');
|
||||||
|
assert.equal(personalized, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a short placeholder (<50 lines) also scores 0 voice points', () => {
|
||||||
|
root = makePluginRoot();
|
||||||
|
writeVoice(root, `# Voice Profile (placeholder)\n${SENTINEL}\n`);
|
||||||
|
|
||||||
|
const { score } = calculateScore(root);
|
||||||
|
|
||||||
|
assert.equal(score, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -16,11 +16,15 @@ export function calculateScore(pluginRoot) {
|
||||||
const categories = 8;
|
const categories = 8;
|
||||||
|
|
||||||
// --- 1. Voice samples (25 points) ---
|
// --- 1. Voice samples (25 points) ---
|
||||||
|
// The shipped file is a PII-free placeholder carrying the VOICE_PLACEHOLDER
|
||||||
|
// sentinel. Key detection on the sentinel (deterministic) rather than the old
|
||||||
|
// `[Your Name]` heuristic: a populated profile removes the sentinel and earns
|
||||||
|
// the points; the placeholder (or any file still carrying it) scores 0.
|
||||||
const voiceFile = join(pluginRoot, 'assets', 'voice-samples', 'authentic-voice-samples.md');
|
const voiceFile = join(pluginRoot, 'assets', 'voice-samples', 'authentic-voice-samples.md');
|
||||||
if (existsSync(voiceFile)) {
|
if (existsSync(voiceFile)) {
|
||||||
const content = readFileSync(voiceFile, 'utf-8');
|
const content = readFileSync(voiceFile, 'utf-8');
|
||||||
const lineCount = content.split('\n').length;
|
const lineCount = content.split('\n').length;
|
||||||
if (lineCount > 50 && !content.includes('[Your Name]')) {
|
if (lineCount > 50 && !content.includes('<!-- VOICE_PLACEHOLDER -->')) {
|
||||||
score += 25;
|
score += 25;
|
||||||
personalized += 1;
|
personalized += 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue