Files
our-claude-skills/docs/superpowers/plans/2026-03-29-jamie-journal-editor-code-version.md
Andrew Yim 8b98fdd2fc feat(jamie-journal): merge desktop SKILL.md, align code CLAUDE.md
Desktop: merged SKILL-org.md (base) + SKILL-new.md additions into SKILL.md
- Added Workflow Modes (full drafting + manuscript editing)
- Added SEO metadata, Visual DNA, Featured Image rules
- Added Gemini prompt templates (4 types), Schema data, 3-file output
- Preserved all original brand voice, analogies, procedures, numerics

Code: rewrote CLAUDE.md to align with desktop (1,195 words)
- Same section coverage, condensed for directive limit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:21:22 +09:00

39 KiB
Raw Permalink Blame History

Jamie Journal Editor Code Version — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build a self-contained Claude Code version of skill 46 (jamie-journal-editor) with a 4-module journal validator CLI tool.

Architecture: Single Python file (journal_validator.py) containing four validator classes (StructureValidator, ComplianceChecker, SpellingChecker, LinkRecommender) orchestrated by a main JournalValidator class. Self-contained CLAUDE.md directive with all brand voice and compliance rules inline.

Tech Stack: Python 3.8+ (stdlib only: re, json, argparse, dataclasses), optional py-hanspell


Task 1: Create journal_validator.py scaffold with CLI and report structure

Files:

  • Create: custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py

  • Step 1: Create the scripts directory

mkdir -p custom-skills/46-jamie-journal-editor/code/scripts
  • Step 2: Write the scaffold with CLI, report structure, and JournalValidator orchestrator

Create custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py:

"""
Jamie Journal Editor - Journal Validator
=========================================

Validates journal articles for Jamie Clinic's "정기호의 성형외과 진료실 이야기"
(journal.jamie.clinic) against structure, compliance, spelling, and internal linking rules.

Usage:
    python journal_validator.py --input article.md
    python journal_validator.py --input article.md --check structure,compliance
    python journal_validator.py --input article.md --verbose --output report.json

Available checks: structure, compliance, spelling, links (default: all)
"""

import re
import json
import sys
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, asdict, field
from argparse import ArgumentParser


@dataclass
class ValidationIssue:
    """A single validation issue found in content."""
    checker: str  # 'structure', 'compliance', 'spelling', 'links'
    issue_type: str
    severity: str  # 'critical', 'high', 'medium', 'info'
    line_number: int  # -1 if not applicable
    matched_text: str
    message: str
    suggestion: str


class JournalValidator:
    """Orchestrates all validation checks on a journal article."""

    VALID_CHECKS = {'structure', 'compliance', 'spelling', 'links'}

    def __init__(self, checks: Optional[List[str]] = None):
        self.checks = set(checks) if checks else self.VALID_CHECKS
        invalid = self.checks - self.VALID_CHECKS
        if invalid:
            raise ValueError(f"Unknown checks: {invalid}. Valid: {self.VALID_CHECKS}")

    def validate(self, content: str) -> Dict:
        """Run selected checks and return a combined report."""
        all_issues: List[ValidationIssue] = []

        if 'structure' in self.checks:
            all_issues.extend(StructureValidator().check(content))
        if 'compliance' in self.checks:
            all_issues.extend(ComplianceChecker().check(content))
        if 'spelling' in self.checks:
            all_issues.extend(SpellingChecker().check(content))

        link_recs = []
        if 'links' in self.checks:
            link_recs = LinkRecommender().check(content)
            all_issues.extend(link_recs)

        severity_counts = {'critical': 0, 'high': 0, 'medium': 0, 'info': 0}
        for issue in all_issues:
            severity_counts[issue.severity] += 1

        has_blocking = severity_counts['critical'] > 0 or severity_counts['high'] > 0

        return {
            'is_valid': not has_blocking,
            'total_issues': len(all_issues),
            'issues_by_severity': severity_counts,
            'issues': [asdict(i) for i in all_issues],
            'summary': self._summary(severity_counts, has_blocking),
        }

    def _summary(self, counts: Dict[str, int], has_blocking: bool) -> str:
        if counts['critical'] == 0 and counts['high'] == 0 and counts['medium'] == 0 and counts['info'] == 0:
            return "All checks passed. Article is ready for brand audit."
        parts = []
        if counts['critical']:
            parts.append(f"{counts['critical']} critical")
        if counts['high']:
            parts.append(f"{counts['high']} high")
        if counts['medium']:
            parts.append(f"{counts['medium']} medium")
        if counts['info']:
            parts.append(f"{counts['info']} info")
        status = "BLOCKED — fix critical/high issues" if has_blocking else "Passable — review medium/info items"
        return f"{status}. Issues: {', '.join(parts)}."


# --- Validator stubs (implemented in subsequent tasks) ---

class StructureValidator:
    def check(self, content: str) -> List[ValidationIssue]:
        return []

class ComplianceChecker:
    def check(self, content: str) -> List[ValidationIssue]:
        return []

class SpellingChecker:
    def check(self, content: str) -> List[ValidationIssue]:
        return []

class LinkRecommender:
    def check(self, content: str) -> List[ValidationIssue]:
        return []


# --- CLI ---

def main():
    parser = ArgumentParser(description='Validate Jamie Clinic journal articles')
    parser.add_argument('--input', '-i', required=True, help='Input article file (markdown)')
    parser.add_argument('--check', '-c', default='all',
                        help='Comma-separated checks: structure,compliance,spelling,links (default: all)')
    parser.add_argument('--output', '-o', help='Save JSON report to file')
    parser.add_argument('--verbose', '-v', action='store_true', help='Print detailed output')
    args = parser.parse_args()

    try:
        with open(args.input, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        print(f"Error: File not found: {args.input}")
        sys.exit(2)

    checks = None if args.check == 'all' else [c.strip() for c in args.check.split(',')]

    try:
        validator = JournalValidator(checks=checks)
    except ValueError as e:
        print(f"Error: {e}")
        sys.exit(2)

    report = validator.validate(content)

    # Print summary
    print("Jamie Journal Validator")
    print("=" * 44)
    print(f"File: {args.input}")
    print(f"Valid: {'YES' if report['is_valid'] else 'NO'}")
    print(f"Total Issues: {report['total_issues']}")
    print(f"  Critical: {report['issues_by_severity']['critical']}")
    print(f"  High:     {report['issues_by_severity']['high']}")
    print(f"  Medium:   {report['issues_by_severity']['medium']}")
    print(f"  Info:     {report['issues_by_severity']['info']}")
    print(f"\n{report['summary']}")

    if args.verbose and report['issues']:
        print("\nIssues Found:")
        for issue in report['issues']:
            line_info = f"L{issue['line_number']}" if issue['line_number'] > 0 else "—"
            print(f"\n  [{issue['severity'].upper()}] {issue['checker']}/{issue['issue_type']} ({line_info})")
            if issue['matched_text']:
                print(f"  Text: '{issue['matched_text']}'")
            print(f"  {issue['message']}")
            if issue['suggestion']:
                print(f"  -> {issue['suggestion']}")

    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            json.dump(report, f, ensure_ascii=False, indent=2)
        print(f"\nReport saved to: {args.output}")

    sys.exit(0 if report['is_valid'] else 1)


if __name__ == '__main__':
    main()
  • Step 3: Verify scaffold runs
cd custom-skills/46-jamie-journal-editor
echo "테스트 문서입니다." > /tmp/test_article.md
python code/scripts/journal_validator.py --input /tmp/test_article.md

Expected output: all zeros, "All checks passed."

  • Step 4: Commit
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
git commit -m "feat(jamie-journal): scaffold journal_validator.py with CLI and orchestrator"

Task 2: Implement StructureValidator

Files:

  • Modify: custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py (replace StructureValidator stub)

  • Step 1: Replace the StructureValidator stub with full implementation

Replace the class StructureValidator stub with:

class StructureValidator:
    """Validates journal article structure against the 5-step content pattern."""

    STANDARD_OPENING = "안녕하세요. 제이미성형외과 정기호 원장입니다."

    # Keywords that signal each of the 5 body steps
    STEP_KEYWORDS = {
        'problem': ['고민', '증상', '불편', '걱정', '힘드', '어려움'],
        'cause': ['원인', '발생', '때문에', '이유', '구조적', '해부학적'],
        'solution': ['시술', '수술', '방법', '제이미', '교정', '개선법'],
        'advantages': ['장점', '회복', '흉터', '통증', '마취', '보장', 'AS'],
        'results': ['결과', '효과', '기대', '개선', '자연스러', '만족'],
    }

    STEP_NAMES_KR = {
        'problem': '문제 제기 (공감)',
        'cause': '원인 설명 (교육)',
        'solution': '해결책 제시 (제이미의 방법)',
        'advantages': '장점 나열 (차별점)',
        'results': '기대 효과 (비전)',
    }

    MEDICAL_TERM_PATTERN = re.compile(
        r'[가-힣]+\([一-龥々〇]+,\s*[A-Za-z\s\-]+\)'
    )

    CTA_PATTERNS = [
        re.compile(r'상담을?\s*(추천|받아|예약)'),
        re.compile(r'제이미성형외과.{0,20}(상담|문의)'),
        re.compile(r'편안한\s*마음으로\s*상담'),
    ]

    DISCLAIMER_FRAGMENTS = [
        '부작용',
        '출혈',
        '감염',
        '의료진과 상담',
    ]

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        issues.extend(self._check_opening(content, lines))
        issues.extend(self._check_body_structure(content))
        issues.extend(self._check_medical_terms(content, lines))
        issues.extend(self._check_closing_cta(content, lines))
        issues.extend(self._check_disclaimer(content, lines))

        return issues

    def _check_opening(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        if self.STANDARD_OPENING not in content:
            return [ValidationIssue(
                checker='structure',
                issue_type='missing_opening',
                severity='high',
                line_number=1,
                matched_text='',
                message='표준 인사말이 누락되었습니다.',
                suggestion=f'글 시작에 "{self.STANDARD_OPENING}" 를 추가하세요.',
            )]
        return []

    def _check_body_structure(self, content: str) -> List[ValidationIssue]:
        detected_steps = []
        missing_steps = []

        for step, keywords in self.STEP_KEYWORDS.items():
            found = any(kw in content for kw in keywords)
            if found:
                detected_steps.append(step)
            else:
                missing_steps.append(step)

        issues = []
        if len(detected_steps) < 4:
            missing_names = [self.STEP_NAMES_KR[s] for s in missing_steps]
            issues.append(ValidationIssue(
                checker='structure',
                issue_type='incomplete_body',
                severity='high',
                line_number=-1,
                matched_text='',
                message=f'5단계 본론 구조 중 {len(missing_steps)}개 단계가 감지되지 않았습니다: {", ".join(missing_names)}',
                suggestion='누락된 단계를 본문에 추가하세요. 각 단계는 소제목(**굵은 글씨**)이나 관련 키워드로 표현할 수 있습니다.',
            ))
        elif missing_steps:
            missing_names = [self.STEP_NAMES_KR[s] for s in missing_steps]
            issues.append(ValidationIssue(
                checker='structure',
                issue_type='partial_body',
                severity='medium',
                line_number=-1,
                matched_text='',
                message=f'5단계 중 {len(detected_steps)}개 감지됨. 누락 가능: {", ".join(missing_names)}',
                suggestion='선택적이지만, 모든 5단계를 포함하면 더 완성도 높은 글이 됩니다.',
            ))

        return issues

    def _check_medical_terms(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        # Check if article mentions medical terms that should use the Korean(漢字, English) format
        # Only flag if there are procedure-like terms without the format
        medical_nouns = ['안검하수', '내시경', '거상술', '눈매교정', '매몰법', '절개법']
        has_formal_term = bool(self.MEDICAL_TERM_PATTERN.search(content))
        has_medical_noun = any(noun in content for noun in medical_nouns)

        if has_medical_noun and not has_formal_term:
            return [ValidationIssue(
                checker='structure',
                issue_type='medical_term_format',
                severity='medium',
                line_number=-1,
                matched_text='',
                message='의학 용어가 있지만 "한글(漢字, English)" 형식의 정식 소개가 없습니다.',
                suggestion='첫 등장 시 "안검하수(眼瞼下垂, Ptosis)는..." 형식으로 소개하세요.',
            )]
        return []

    def _check_closing_cta(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        # Check last 20% of content for CTA
        tail_start = int(len(content) * 0.8)
        tail = content[tail_start:]

        has_cta = any(p.search(tail) for p in self.CTA_PATTERNS)
        if not has_cta:
            return [ValidationIssue(
                checker='structure',
                issue_type='missing_cta',
                severity='medium',
                line_number=-1,
                matched_text='',
                message='글 마무리에 상담 CTA가 감지되지 않았습니다.',
                suggestion='"[고민]이시라면 지금 바로 제이미성형외과의 [시술명] 상담을 추천드립니다." 패턴을 추가하세요.',
            )]
        return []

    def _check_disclaimer(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        # Check last 30% of content for disclaimer
        tail_start = int(len(content) * 0.7)
        tail = content[tail_start:]

        has_disclaimer = sum(1 for frag in self.DISCLAIMER_FRAGMENTS if frag in tail) >= 2
        if not has_disclaimer:
            return [ValidationIssue(
                checker='structure',
                issue_type='missing_disclaimer',
                severity='high',
                line_number=-1,
                matched_text='',
                message='필수 고지문이 누락되었습니다.',
                suggestion='"개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다." 를 글 하단에 추가하세요.',
            )]
        return []
  • Step 2: Test with a minimal article

Create /tmp/test_journal.md:

안녕하세요. 제이미성형외과 정기호 원장입니다.

오늘은 눈꺼풀 처짐으로 고민하시는 분들을 위한 안검하수 눈매교정에 대해 말씀드리겠습니다.

안검하수(眼瞼下垂, Ptosis)는 눈을 뜨는 근육의 힘이 약해져 눈꺼풀이 처지는 현상을 말합니다.

이러한 증상의 원인은 선천적 또는 후천적으로 발생할 수 있습니다.

제이미성형외과에서는 정밀 진단을 통해 가장 적합한 수술 방법을 찾아드립니다.

수술 후 회복 기간은 약 일주일 정도이며, 흉터가 거의 보이지 않는 것이 장점입니다.

자연스러운 눈매 개선 결과를 기대하실 수 있습니다.

눈매가 답답하시다면 지금 바로 제이미성형외과의 눈매교정 상담을 추천드립니다.

개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
python code/scripts/journal_validator.py --input /tmp/test_journal.md --verbose

Expected: Valid = YES, 0 critical/high issues. May show info-level link recommendations.

  • Step 3: Test with a broken article (no opening, no disclaimer)

Create /tmp/test_bad.md:

눈 성형에 대해 알아보겠습니다.

수술 방법은 다양합니다.
python code/scripts/journal_validator.py --input /tmp/test_bad.md --verbose

Expected: Valid = NO, high issues for missing_opening, incomplete_body, missing_disclaimer.

  • Step 4: Commit
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
git commit -m "feat(jamie-journal): implement StructureValidator (5-step body, opening, CTA, disclaimer)"

Task 3: Implement ComplianceChecker

Files:

  • Modify: custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py (replace ComplianceChecker stub)

  • Step 1: Replace the ComplianceChecker stub with full implementation

Replace the class ComplianceChecker stub with:

class ComplianceChecker:
    """Checks content for Korean medical advertising law violations (의료법 제56조)."""

    PROHIBITED_PATTERNS = {
        'effect_guarantee': {
            'severity': 'critical',
            'patterns': [
                r'100[%]\s*(?:만족|효과|성공)',
                r'반드시\s+(?:효과|개선|만족)',
                r'완벽한?\s+결과',
                r'보장합니다',
                r'확실한?\s+효과',
                r'틀림없이?\s+(?:효과|개선)',
            ],
            'message': '효과를 보장하는 표현은 의료법 제56조 제2항 제2호 위반입니다.',
            'suggestion': '"개선에 도움을 줄 수 있습니다" 또는 "개인에 따라 결과가 다를 수 있습니다"로 변경하세요.',
        },
        'comparative_superiority': {
            'severity': 'critical',
            'patterns': [
                r'최고의?',
                r'(?:국내|강남|서울|압구정)\s*(?:최고|1위|최초)',
                r'1위',
                r'타\s*병원보다',
                r'다른\s*(?:병원|의원)보다\s*(?:우수|뛰어|좋)',
                r'업계\s*최초',
                r'독보적',
            ],
            'message': '타 의료기관과의 비교 우위 주장은 의료법 제56조 제2항 제4호 위반입니다.',
            'suggestion': '"풍부한 경험을 보유한" 또는 "저희만의 방법으로"와 같은 객관적 표현으로 변경하세요.',
        },
        'safety_guarantee': {
            'severity': 'critical',
            'patterns': [
                r'부작용\s*(?:없|無|제로|zero)',
                r'(?:100[%]|완전히?|절대)\s*안전',
                r'위험\s*(?:없|無)',
                r'흉터\s*(?:없|無|제로|zero)',
                r'무통(?:증)?(?:\s|$|,)',
            ],
            'message': '안전성을 보장하는 표현은 의료법 제56조 제2항 제7호 위반입니다.',
            'suggestion': '부작용 가능성을 명시하고 "안전한 수술을 위해 최선을 다합니다"로 변경하세요.',
        },
        'patient_testimonial': {
            'severity': 'critical',
            'patterns': [
                r'(?:환자|고객)\s*[가-힣A-Z]+\s*(?:씨|님)의?\s*(?:후기|경험)',
                r'실제\s*(?:환자|고객)\s*(?:후기|리뷰|경험담)',
                r'[""]\s*(?:정말|너무|진짜)\s+(?:만족|좋아요|감사)',
                r'수술\s*후\s*\d+\s*(?:개월|주일|년)\s*(?:만족|경과)',
            ],
            'message': '환자 치료경험담은 의료법 제56조 제2항 제2호 위반입니다.',
            'suggestion': '환자 후기를 제거하고, 일반적인 수술 과정 설명으로 대체하세요.',
        },
        'exaggeration': {
            'severity': 'high',
            'patterns': [
                r'(?:놀라운|대박|극적인)\s*(?:변화|효과|결과)',
                r'마법같은?',
                r'기적적인?',
                r'다시\s*태어나',
                r'완벽\s*변신',
            ],
            'message': '과장된 표현은 의료법 제56조 제2항 제3호 위반 가능성이 있습니다.',
            'suggestion': '"자연스러운 개선", "점진적인 효과"와 같은 절제된 표현으로 변경하세요.',
        },
        'specialty_claim': {
            'severity': 'high',
            'patterns': [
                r'전문\s*병원',
                r'특화\s*(?:병원|클리닉|센터)',
                r'노하우',
            ],
            'message': '전문병원 지정 없이 "전문", "특화" 표현 사용은 과장 광고에 해당합니다.',
            'suggestion': '"중점진료", "집중진료", "풍부한 경험"으로 변경하세요.',
        },
    }

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        # Check prohibited patterns
        for vtype, config in self.PROHIBITED_PATTERNS.items():
            for pattern in config['patterns']:
                for match in re.finditer(pattern, content, re.IGNORECASE):
                    line_num = content[:match.start()].count('\n') + 1
                    issues.append(ValidationIssue(
                        checker='compliance',
                        issue_type=vtype,
                        severity=config['severity'],
                        line_number=line_num,
                        matched_text=match.group(),
                        message=config['message'],
                        suggestion=config['suggestion'],
                    ))

        # Check missing disclaimers for procedure content
        procedure_keywords = ['수술', '시술', '이마거상', '쌍꺼풀', '리프팅', '보톡스', '필러',
                              '눈매교정', '안검하수', '지방이식', '절개']
        has_procedure = any(kw in content for kw in procedure_keywords)

        if has_procedure:
            has_side_effect = any(t in content for t in ['부작용', '합병증', '붓기', '멍', '염증'])
            has_individual_var = '개인' in content and any(t in content for t in ['차이', '다를 수', '다릅니다'])

            if not has_side_effect:
                issues.append(ValidationIssue(
                    checker='compliance',
                    issue_type='missing_side_effect_notice',
                    severity='high',
                    line_number=-1,
                    matched_text='',
                    message='부작용 가능성에 대한 고지가 누락되었습니다.',
                    suggestion='"※ 모든 수술 및 시술은 개인에 따라 붓기, 멍, 염증 등의 부작용이 발생할 수 있습니다." 문구를 추가하세요.',
                ))

            if not has_individual_var:
                issues.append(ValidationIssue(
                    checker='compliance',
                    issue_type='missing_individual_variation',
                    severity='medium',
                    line_number=-1,
                    matched_text='',
                    message='개인차에 대한 고지가 누락되었습니다.',
                    suggestion='"개인에 따라 결과가 다를 수 있습니다" 문구를 추가하세요.',
                ))

        return issues
  • Step 2: Test compliance with a bad article

Create /tmp/test_compliance.md:

안녕하세요. 제이미성형외과 정기호 원장입니다.

저희 병원은 강남 최고의 눈 성형 전문 병원입니다.
100% 만족을 보장합니다. 부작용 없는 안전한 수술!

실제 환자 김OO님의 후기: "정말 만족합니다!"

놀라운 변화를 경험하세요. 마법같은 결과!
python code/scripts/journal_validator.py --input /tmp/test_compliance.md --check compliance --verbose

Expected: Multiple critical violations (effect_guarantee, comparative_superiority, safety_guarantee, patient_testimonial, exaggeration, specialty_claim).

  • Step 3: Commit
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
git commit -m "feat(jamie-journal): implement ComplianceChecker (의료법 제56조 pattern matching)"

Task 4: Implement SpellingChecker

Files:

  • Modify: custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py (replace SpellingChecker stub)

  • Step 1: Replace the SpellingChecker stub with full implementation

Replace the class SpellingChecker stub with:

class SpellingChecker:
    """Two-tier Korean spelling checker for medical/cosmetic content."""

    # Tier 1: Built-in medical term misspelling dictionary
    # Format: { wrong_spelling: (correct_spelling, category) }
    MISSPELLINGS = {
        '쌍커풀': ('쌍꺼풀', '눈 성형'),
        '쌍거풀': ('쌍꺼풀', '눈 성형'),
        '안겸하수': ('안검하수', '눈 성형'),
        '안검하수증': ('안검하수', '눈 성형'),
        '눈꺼플': ('눈꺼풀', '눈 성형'),
        '눈커풀': ('눈꺼풀', '눈 성형'),
        '네시경': ('내시경', '이마 성형'),
        '거상 술': ('거상술', '이마 성형'),
        '앤도타인': ('엔도타인', '이마 성형'),
        '엔도타인': ('엔도타인', '이마 성형'),  # correct - for reference only
        '봉합 사': ('봉합사', '이마 성형'),
        '스마스층': ('SMAS층', '동안 성형'),
        '히알루론 산': ('히알루론산', '필러'),
        '히알류론산': ('히알루론산', '필러'),
        '보톡스': ('보톡스', '정상'),  # correct - skip
        '보톡스주': ('보톡스 주사', '보톡스'),
        '리프팅': ('리프팅', '정상'),  # correct - skip
        '리프팅술': ('리프팅', '동안 성형'),
        '하이푸': ('하이푸', '정상'),  # correct - skip
        '지방이식': ('지방이식', '정상'),  # correct - skip
        '지방 이식': ('지방이식', '동안 성형'),
        '눈밑지방': ('눈밑 지방', '눈 성형'),
        '다크 서클': ('다크서클', '눈 성형'),
        '팔자주름': ('팔자 주름', '동안 성형'),
        '절개법': ('절개법', '정상'),  # correct - skip
        '절게법': ('절개법', '눈 성형'),
        '매몰법': ('매몰법', '정상'),  # correct - skip
        '매몰 법': ('매몰법', '눈 성형'),
        '눈썹밑절개': ('눈썹밑 절개', '이마 성형'),
    }

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        # Tier 1: Built-in dictionary check
        issues.extend(self._check_builtin(content, lines))

        # Tier 2: py-hanspell (optional)
        issues.extend(self._check_hanspell(content, lines))

        return issues

    def _check_builtin(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        issues = []
        for wrong, (correct, category) in self.MISSPELLINGS.items():
            if category == '정상':
                continue  # Skip entries marked as correct reference
            if wrong == correct:
                continue

            for i, line in enumerate(lines, 1):
                if wrong in line:
                    issues.append(ValidationIssue(
                        checker='spelling',
                        issue_type='medical_misspelling',
                        severity='medium',
                        line_number=i,
                        matched_text=wrong,
                        message=f'의료 용어 오타: "{wrong}" → "{correct}" ({category})',
                        suggestion=f'"{wrong}"을(를) "{correct}"(으)로 수정하세요.',
                    ))
        return issues

    def _check_hanspell(self, content: str, lines: List[str]) -> List[ValidationIssue]:
        try:
            from hanspell import spell_checker
        except ImportError:
            return [ValidationIssue(
                checker='spelling',
                issue_type='hanspell_unavailable',
                severity='info',
                line_number=-1,
                matched_text='',
                message='py-hanspell이 설치되지 않아 맞춤법 검사를 건너뜁니다.',
                suggestion='더 정밀한 맞춤법 검사를 원하시면: pip install py-hanspell',
            )]

        issues = []
        # Check non-empty lines, skip code blocks and short lines
        in_code_block = False
        for i, line in enumerate(lines, 1):
            stripped = line.strip()
            if stripped.startswith('```'):
                in_code_block = not in_code_block
                continue
            if in_code_block or len(stripped) < 5 or stripped.startswith('#'):
                continue

            try:
                result = spell_checker.check(stripped)
                if result.errors > 0:
                    for word, correction in zip(result.words.keys(), result.words.values()):
                        if word != correction:
                            issues.append(ValidationIssue(
                                checker='spelling',
                                issue_type='hanspell_correction',
                                severity='medium',
                                line_number=i,
                                matched_text=word,
                                message=f'맞춤법 교정 제안: "{word}" → "{correction}"',
                                suggestion=f'"{word}"을(를) "{correction}"(으)로 수정하세요.',
                            ))
            except Exception:
                # hanspell can fail on network issues; don't block validation
                pass

        return issues
  • Step 2: Test spelling checker with misspellings

Create /tmp/test_spelling.md:

안녕하세요. 제이미성형외과 정기호 원장입니다.

쌍커풀 수술과 안겸하수 눈매교정에 대해 설명드리겠습니다.

네시경을 이용한 거상 술은 앤도타인 고정 방식을 사용합니다.

개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
python code/scripts/journal_validator.py --input /tmp/test_spelling.md --check spelling --verbose

Expected: Medium issues for 쌍커풀→쌍꺼풀, 안겸하수→안검하수, 네시경→내시경, 거상 술→거상술, 앤도타인→엔도타인. Plus info about hanspell unavailable (unless installed).

  • Step 3: Commit
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
git commit -m "feat(jamie-journal): implement SpellingChecker (built-in medical dict + optional hanspell)"

Task 5: Implement LinkRecommender

Files:

  • Modify: custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py (replace LinkRecommender stub)

  • Step 1: Replace the LinkRecommender stub with full implementation

Replace the class LinkRecommender stub with:

class LinkRecommender:
    """Scans for procedure mentions and suggests internal linking opportunities."""

    # Procedure keyword groups: { topic_key: (display_name, [keywords]) }
    PROCEDURE_MAP = {
        'double_eyelid': ('쌍꺼풀 수술', ['쌍꺼풀', '매몰법', '절개법', '퀵매몰']),
        'ptosis': ('안검하수 눈매교정', ['안검하수', '눈매교정', '졸린 눈', '눈매 교정']),
        'under_eye_fat': ('눈밑 지방 재배치', ['눈밑 지방', '다크서클', '눈밑지방', '지방 재배치']),
        'dual_epicanthoplasty': ('듀얼 트임', ['듀얼 트임', '앞트임', '뒤트임', '눈꼬리']),
        'forehead_lift': ('내시경 이마거상술', ['이마거상', '이마리프팅', '이마 거상', '이마 리프팅']),
        'brow_lift': ('내시경 눈썹거상술', ['눈썹거상', '눈썹리프팅', '눈썹 거상', '눈썹 리프팅']),
        'sub_brow': ('눈썹밑 피부절개술', ['눈썹밑', '상안검', '눈꺼풀 처짐']),
        'smas_lifting': ('스마스 리프팅', ['스마스', 'SMAS', '안면거상', '페이스리프트']),
        'fat_graft': ('자가지방 이식', ['자가지방', '지방이식', '지방 이식', '지방 생착']),
        'hifu': ('하이푸 리프팅', ['하이푸', 'HIFU', '초음파 리프팅']),
        'cheek_lift': ('앞광대 리프팅', ['앞광대', '팔자 주름', '팔자주름', '광대 리프팅']),
        'endotine': ('엔도타인 고정', ['엔도타인', 'endotine', '3점 고정']),
    }

    def check(self, content: str) -> List[ValidationIssue]:
        issues: List[ValidationIssue] = []
        lines = content.split('\n')

        # Detect the article's main topic (first procedure mentioned in title/opening)
        main_topic = self._detect_main_topic(lines[:10])

        # Scan for other procedure mentions
        for topic_key, (display_name, keywords) in self.PROCEDURE_MAP.items():
            if topic_key == main_topic:
                continue  # Don't recommend linking to self

            for keyword in keywords:
                for i, line in enumerate(lines, 1):
                    if keyword in line:
                        issues.append(ValidationIssue(
                            checker='links',
                            issue_type='link_opportunity',
                            severity='info',
                            line_number=i,
                            matched_text=keyword,
                            message=f'"{keyword}" 언급 발견 — "{display_name}" 글로의 내부 링크를 고려하세요.',
                            suggestion=f'"{keyword}" 부분에 관련 저널 포스트({display_name}) 링크를 추가하면 독자 탐색에 도움이 됩니다.',
                        ))
                        break  # One recommendation per topic per keyword match

        return issues

    def _detect_main_topic(self, opening_lines: List[str]) -> Optional[str]:
        """Detect the article's main procedure topic from opening lines."""
        opening_text = '\n'.join(opening_lines)
        for topic_key, (_, keywords) in self.PROCEDURE_MAP.items():
            if any(kw in opening_text for kw in keywords):
                return topic_key
        return None
  • Step 2: Test link recommender

Create /tmp/test_links.md:

안녕하세요. 제이미성형외과 정기호 원장입니다.

오늘은 안검하수 눈매교정에 대해 말씀드리겠습니다.

눈매교정과 함께 쌍꺼풀 수술을 병행하는 경우도 있습니다.

이마거상술과 함께 진행하면 더욱 시원한 눈매를 기대할 수 있습니다.

자가지방 이식을 통해 볼륨감을 보완하기도 합니다.

상담을 추천드립니다.

개인에 따라 부작용(출혈, 감염, 염증 등)이 있을 수 있으니 사전에 의료진과 상담 후 결정하시기 바랍니다.
python code/scripts/journal_validator.py --input /tmp/test_links.md --check links --verbose

Expected: Info-level recommendations for 쌍꺼풀 (link to double eyelid article), 이마거상술 (link to forehead lift article), 자가지방 (link to fat graft article). Should NOT recommend linking 안검하수 since that's the main topic.

  • Step 3: Commit
git add custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py
git commit -m "feat(jamie-journal): implement LinkRecommender (procedure cross-linking suggestions)"

Task 6: Rewrite code/CLAUDE.md as self-contained directive

Files:

  • Modify: custom-skills/46-jamie-journal-editor/code/CLAUDE.md

  • Step 1: Rewrite CLAUDE.md with all brand voice, compliance, and structure rules inline

Replace the entire contents of custom-skills/46-jamie-journal-editor/code/CLAUDE.md with the self-contained directive. This must NOT reference desktop/references/.

Content sections:

  1. Overview + Quick Start (validator CLI usage)
  2. Brand Voice — personality traits, honorifics, hospital naming, standard opening, CTA patterns
  3. Analogy Dictionary — Dr. Jung's signature metaphors
  4. Content Structure — 5-step body, opening/closing templates
  5. Procedure Copy Reference — eye, forehead, anti-aging key expressions
  6. Numeric Expressions & Medical Term Pattern
  7. Compliance Rules — prohibited expressions (table), required disclaimers
  8. Do's & Don'ts
  9. Image Placement — 5 positions
  10. Workflow — Topic → Draft → Validate → Brand Audit

Word budget: ~1,200 words (under 1,500-word limit).

Full content provided in implementation (too large for plan — this is a prose document, not code logic).

  • Step 2: Verify word count
wc -w custom-skills/46-jamie-journal-editor/code/CLAUDE.md

Expected: Under 1,500 words.

  • Step 3: Commit
git add custom-skills/46-jamie-journal-editor/code/CLAUDE.md
git commit -m "feat(jamie-journal): rewrite code/CLAUDE.md as self-contained directive"

Task 7: Update README.md

Files:

  • Modify: custom-skills/46-jamie-journal-editor/README.md

  • Step 1: Update the Structure section to reflect new code/scripts/ directory

Update the structure tree in README.md to include:

46-jamie-journal-editor/
├── README.md
├── code/
│   ├── CLAUDE.md                     # Claude Code directive (self-contained)
│   └── scripts/
│       └── journal_validator.py      # Article validator (structure, compliance, spelling, links)
├── desktop/
│   ├── SKILL.md                      # Claude Desktop skill directive
│   ├── skill.yaml                    # Extended metadata
│   ├── jamie-journal-editor.skill    # Packaged skill (ZIP)
│   └── references/
│       ├── brand-voice.md
│       ├── content-patterns.md
│       └── medical-compliance.md

Add a "Quick Start" section after the overview:

## Quick Start (Claude Code)

\`\`\`bash
# Full validation
python code/scripts/journal_validator.py --input article.md

# Specific checks
python code/scripts/journal_validator.py --input article.md --check structure,compliance

# Verbose JSON report
python code/scripts/journal_validator.py --input article.md --verbose --output report.json
\`\`\`
  • Step 2: Commit
git add custom-skills/46-jamie-journal-editor/README.md
git commit -m "docs(jamie-journal): update README with code version structure and quick start"

Task 8: Integration test and push

  • Step 1: Run full validation against the good test article
python custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input /tmp/test_journal.md --verbose --output /tmp/journal_report.json

Expected: Valid = YES or only info-level issues.

  • Step 2: Run full validation against the bad compliance article
python custom-skills/46-jamie-journal-editor/code/scripts/journal_validator.py --input /tmp/test_compliance.md --verbose

Expected: Valid = NO, multiple critical issues.

  • Step 3: Push to remote
git push origin main
  • Step 4: Clean up test files
rm -f /tmp/test_article.md /tmp/test_journal.md /tmp/test_bad.md /tmp/test_compliance.md /tmp/test_spelling.md /tmp/test_links.md /tmp/journal_report.json