scripts/__init__.py
"""genshijin-compress scripts.
自然言語Markdownファイルを原始人形式に圧縮し入力トークン削減するツール群。
"""
import json
from pathlib import Path
__all__ = ["cli", "compress", "detect", "validate"]
def _read_plugin_version() -> str:
# plugin.json が single source of truth。
# パッケージ構造: <repo>/skills/genshijin-compress/scripts/__init__.py
# plugin.json: <repo>/.claude-plugin/plugin.json
manifest = Path(__file__).resolve().parents[3] / ".claude-plugin" / "plugin.json"
try:
return json.loads(manifest.read_text())["version"]
except (FileNotFoundError, KeyError, json.JSONDecodeError):
return "0.0.0"
__version__ = _read_plugin_version()
scripts/__main__.py
from .cli import main
main()
scripts/cli.py
#!/usr/bin/env python3
"""
genshijin-compress CLI
使い方:
genshijin-compress <filepath>
"""
import sys
from pathlib import Path
from .compress import compress_file
from .detect import detect_file_type, should_compress
def print_usage():
print("使い方: genshijin-compress <filepath>")
def main():
if len(sys.argv) != 2:
print_usage()
sys.exit(1)
filepath = Path(sys.argv[1])
if not filepath.exists():
print(f"❌ ファイルが見つかりません: {filepath}")
sys.exit(1)
if not filepath.is_file():
print(f"❌ ファイルではありません: {filepath}")
sys.exit(1)
filepath = filepath.resolve()
file_type = detect_file_type(filepath)
print(f"検出: {file_type}")
if not should_compress(filepath):
print("スキップ: 自然言語ファイルではありません(コード/設定ファイル)")
sys.exit(0)
print("原始人圧縮 開始...\n")
try:
success = compress_file(filepath)
if success:
print("\n圧縮完了")
backup_path = filepath.with_name(filepath.stem + ".original.md")
print(f"圧縮版: {filepath}")
print(f"バックアップ: {backup_path}")
sys.exit(0)
else:
print("\n❌ リトライ後も圧縮失敗")
sys.exit(2)
except KeyboardInterrupt:
print("\nユーザー中断")
sys.exit(130)
except Exception as e:
print(f"\n❌ エラー: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
scripts/compress.py
#!/usr/bin/env python3
"""
genshijin メモリ圧縮オーケストレータ
使い方:
python scripts/compress.py <filepath>
"""
import io
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List
# Windows 環境 cp932 stdout で日本語/特殊文字 UnicodeEncodeError 回避。
# Python 3.7+ は reconfigure 利用可。古い環境は io.TextIOWrapper でラップ。
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, io.UnsupportedOperation):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
OUTER_FENCE_REGEX = re.compile(
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
)
# YAML frontmatter。圧縮対象から外し、byte-equivalent な文字列として復元。
FRONTMATTER_REGEX = re.compile(
r"\A(---\r?\n.*?\r?\n---\r?\n)(.*)", re.DOTALL
)
# 機密・PII を含む可能性高いファイル名/パス。圧縮すると Anthropic API に生データ送信 →
# 機密リポジトリでは越えられない第三者データ境界。detect.py は .env を拡張子で弾くが、
# credentials.md / secrets.txt / ~/.aws/credentials は自然言語フィルタをすり抜ける。
# read 前にハード拒否する。
SENSITIVE_BASENAME_REGEX = re.compile(
r"(?ix)^("
r"\.env(\..+)?"
r"|\.netrc"
r"|credentials(\..+)?"
r"|secrets?(\..+)?"
r"|passwords?(\..+)?"
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
r"|authorized_keys"
r"|known_hosts"
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
r")$"
)
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
SENSITIVE_NAME_TOKENS = (
"secret", "credential", "password", "passwd",
"apikey", "accesskey", "token", "privatekey",
)
def is_sensitive_path(filepath: Path) -> bool:
"""第三者API送信厳禁ファイルのヒューリスティック拒否リスト。"""
name = filepath.name
if SENSITIVE_BASENAME_REGEX.match(name):
return True
lowered_parts = {p.lower() for p in filepath.parts}
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
return True
# "api-key" と "api_key" 両方を "apikey" にマッチさせるため区切り文字正規化
lower = re.sub(r"[_\-\s.]", "", name.lower())
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
def strip_llm_wrapper(text: str) -> str:
"""出力全体を包む外側の ```markdown ... ``` フェンスを除去。"""
m = OUTER_FENCE_REGEX.match(text)
if m:
return m.group(2)
return text
def split_frontmatter(text: str):
"""UTF-8 BOM/YAML frontmatter と本文を分離。prefix は無変更で復元。"""
bom = "\ufeff" if text.startswith("\ufeff") else ""
source = text[len(bom):]
m = FRONTMATTER_REGEX.match(source)
if m:
return bom + m.group(1), m.group(2)
if bom:
return bom, source
return "", source
def cleanup_compressed(text: str) -> str:
"""圧縮後テキストの最終整形:
- frontmatter 後の連続空行を1行に
- 末尾改行正規化 (1個)
- 先頭BOM除去
"""
if text.startswith(""):
text = text[1:]
# 末尾改行 1個に
text = text.rstrip() + "\n"
return text
def write_text_atomic(path: Path, text: str) -> None:
"""UTF-8へ先にencodeし、同一ディレクトリの一時ファイルからatomic置換。"""
data = text.encode("utf-8")
fd, tmp_name = tempfile.mkstemp(
dir=str(path.parent), prefix=path.name + ".", suffix=".tmp"
)
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "wb") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
if path.exists():
os.chmod(tmp_path, stat.S_IMODE(path.stat().st_mode))
os.replace(tmp_path, path)
except Exception:
try:
tmp_path.unlink()
except OSError:
pass
raise
def read_text_exact(path: Path, errors: str = "strict") -> str:
"""UTF-8読込。改行変換を無効化し、CRLF/LFをそのまま保持。"""
with path.open("r", encoding="utf-8", errors=errors, newline="") as file:
return file.read()
def first_nonblank_line(text: str) -> str:
for line in text.splitlines():
if line.strip():
return line.strip()
return ""
def _write_target(filepath: Path, text: str, backup_path: Path) -> None:
try:
write_text_atomic(filepath, text)
except Exception:
print(f"❌ {filepath} への書込失敗。原文バックアップ: {backup_path}")
raise
from .detect import should_compress
from .validate import validate
MAX_RETRIES = 2
def call_claude(prompt: str) -> str:
api_key = os.environ.get("ANTHROPIC_API_KEY")
if api_key:
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
msg = client.messages.create(
model=os.environ.get("GENSHIJIN_MODEL", "claude-sonnet-4-5"),
max_tokens=8192,
messages=[{"role": "user", "content": prompt}],
)
return strip_llm_wrapper(msg.content[0].text.strip())
except ImportError:
pass # anthropic未インストール → CLI fallback
# Fallback: claude CLI 使用(デスクトップ認証対応)
claude_bin = shutil.which("claude") or "claude"
try:
result = subprocess.run(
[claude_bin, "--print"],
input=prompt,
text=True,
capture_output=True,
check=True,
encoding="utf-8",
errors="replace",
)
return strip_llm_wrapper(result.stdout.strip())
except subprocess.CalledProcessError as e:
raise RuntimeError(f"Claude 呼出失敗:\n{e.stderr}")
def build_compress_prompt(original: str) -> str:
return f"""以下のMarkdownを原始人モード(genshijin)形式に圧縮してください。
厳守ルール:
- ``` コードブロック内は一切変更しない
- インラインバッククォート内は一切変更しない
- 全URLを正確に保持
- 全見出しを正確に保持
- ファイルパス・コマンドを保持
- 数値・日付・バージョン番号を保持
- 技術用語・ライブラリ名・API名を保持
- エラーメッセージ原文を保持
- 圧縮後のMarkdown本体のみを返す — 出力全体を ```markdown フェンスで包まないこと。原文の内部コードブロックはそのまま残す
圧縮方針(自然言語部分のみ):
- 敬語・丁寧語を削除(です/ます → 体言止め)
- クッション言葉・前置き・ぼかしを削除
- 自明な助詞(が/の/を/に/で/は)を省略
- 形容動詞活用語尾(な/に/で/だ)を語幹止めに
- 形式名詞(こと/もの/ため)を削除 or 名詞化
- 補助動詞(ている/ておく/てしまう)を状態表現に
- 漢字連結で助詞吸収(「高負荷時に高速」→「高負荷時高速」)
- 和語→漢語化で圧縮(「速く動作」→「高速動作」)
- 体言止め・用言止めを許可
- 重複を統合、同パターン複数例は1例のみ
テキスト:
{original}
"""
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
errors_str = "\n".join(f"- {e}" for e in errors)
return f"""genshijin 圧縮済みMarkdownファイルの検証エラー修正タスクです。
重要ルール:
- 再圧縮・言い換え 禁止
- 指摘されたエラーのみ修正 — 他は完全にそのまま
- 原文は参考情報(消失した内容を復元する用途のみ)
- 未変更セクションは原始人スタイル維持
修正対象エラー:
{errors_str}
修正方針:
- URL消失: 原文から見つけて、COMPRESSED の該当位置に正確に復元
- コードブロック不一致: 原文の正確なコードブロックを COMPRESSED に復元
- 見出し不一致: 原文の正確な見出しテキストを COMPRESSED に復元
- エラーに記載のないセクションは絶対に触らない
原文(参考のみ):
{original}
圧縮版(これを修正):
{compressed}
修正後の圧縮ファイルのみ返してください。説明不要。
"""
def compress_file(filepath: Path) -> bool:
filepath = filepath.resolve()
MAX_FILE_SIZE = 500_000 # 500KB
if not filepath.exists():
raise FileNotFoundError(f"ファイルが見つかりません: {filepath}")
if filepath.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"安全圧縮可能サイズ超過(上限500KB): {filepath}")
# 機密・鍵・認証情報ファイルは拒否。圧縮は Anthropic API に生データ送信 =
# 第三者境界越え。サイレント流出を避け、ここで明示的に失敗させる。
# 誤検知時はファイル名変更で回避可能。
if is_sensitive_path(filepath):
raise ValueError(
f"圧縮拒否: {filepath} — ファイル名が機密情報(認証情報・鍵・シークレット・"
"既知のプライベートパス)を示唆します。"
"圧縮はファイル内容を Anthropic API に送信します。"
"誤検知の場合はファイル名を変更してください。"
)
print(f"処理中: {filepath}")
if not should_compress(filepath):
print("スキップ(自然言語ではない)")
return False
original_text = read_text_exact(filepath)
# 空ファイル ガード — Claude API 送信不要、原ファイル無変更
if not original_text.strip():
print("スキップ: 空ファイル")
return False
backup_path = filepath.with_name(filepath.stem + ".original.md")
# バックアップ既存時は誤上書き防止のため中止
if backup_path.exists():
print(f"⚠️ バックアップ既存: {backup_path}")
print("既存バックアップに重要な内容が含まれる可能性あり。")
print("データ損失防止のため中止。続行するには既存バックアップを削除 or リネームしてください。")
return False
# frontmatter は LLM に渡さず、そのまま復元
frontmatter, body = split_frontmatter(original_text)
if frontmatter:
label = "YAML frontmatter" if frontmatter.lstrip("\ufeff").startswith("---") else "UTF-8 BOM"
print(f"{label} 検出({len(frontmatter)}文字)— 無変更で保持")
if not body.strip():
print("スキップ: frontmatter 除去後の本文が空")
return False
# Step 1: 本文のみ圧縮
print("Claude で圧縮中...")
compressed_body = call_claude(build_compress_prompt(body))
if compressed_body is None or not compressed_body.strip():
print("スキップ: Claude が空出力を返したため原ファイル無変更")
return False
compressed_body = cleanup_compressed(compressed_body)
# 同一出力 ガード — Claude が圧縮失敗 or 既に圧縮済の場合バックアップ作らず終了
if compressed_body.strip() == body.strip():
print("スキップ: 圧縮効果なし(既に最小形 or LLM未削減)")
return False
compressed = frontmatter + compressed_body
# 原ファイルをバックアップ、圧縮版を原パスに書き込み
write_text_atomic(backup_path, original_text)
if read_text_exact(backup_path) != original_text:
backup_path.unlink(missing_ok=True)
print("❌ バックアップ検証失敗。原ファイル無変更")
return False
_write_target(filepath, compressed, backup_path)
# Step 2: 検証 + リトライ
for attempt in range(MAX_RETRIES):
print(f"\n検証 {attempt + 1}回目")
result = validate(backup_path, filepath)
if result.is_valid:
print("検証 合格")
break
print("❌ 検証失敗:")
for err in result.errors:
print(f" - {err}")
if attempt == MAX_RETRIES - 1:
# 失敗時は原ファイル復元
_write_target(filepath, original_text, backup_path)
backup_path.unlink(missing_ok=True)
print("❌ リトライ後も失敗 — 原ファイル復元")
return False
print("Claude でピンポイント修正中...")
current_body = compressed[len(frontmatter):] if frontmatter else compressed
fixed_body = call_claude(
build_fix_prompt(body, current_body, result.errors)
)
if fixed_body is None or not fixed_body.strip():
print("❌ 修正出力が空。今回の修正をスキップ")
continue
fixed_body = cleanup_compressed(fixed_body)
anchor = first_nonblank_line(body)
if anchor.startswith("#") and first_nonblank_line(fixed_body) != anchor:
print("❌ 修正出力の先頭構造が原文と不一致。前置き混入の可能性によりスキップ")
continue
compressed = frontmatter + fixed_body
_write_target(filepath, compressed, backup_path)
return True
scripts/detect.py
#!/usr/bin/env python3
"""ファイルが自然言語(圧縮可)かコード/設定(スキップ)かを検出。"""
import json
import re
from pathlib import Path
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst"}
SKIP_EXTENSIONS = {
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
}
KNOWN_CODE_FILENAMES = {
"dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile",
"rakefile", "gemfile", "justfile", "procfile", "brewfile",
"cmakelists.txt",
}
CODE_PATTERNS = [
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
re.compile(r"^\s*(def |class |function |async function |export )"),
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
re.compile(r"^\s*[\}\]\);]+\s*$"),
re.compile(r"^\s*@\w+"),
re.compile(r'^\s*"[^"]+"\s*:\s*'),
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"),
]
def _is_code_line(line: str) -> bool:
return any(p.match(line) for p in CODE_PATTERNS)
def _is_json_content(text: str) -> bool:
try:
json.loads(text)
return True
except (json.JSONDecodeError, ValueError):
return False
def _is_yaml_content(lines: list[str]) -> bool:
yaml_indicators = 0
for line in lines[:30]:
stripped = line.strip()
if stripped.startswith("---"):
yaml_indicators += 1
elif re.match(r"^\w[\w\s]*:\s", stripped):
yaml_indicators += 1
elif stripped.startswith("- ") and ":" in stripped:
yaml_indicators += 1
non_empty = sum(1 for l in lines[:30] if l.strip())
return non_empty > 0 and yaml_indicators / non_empty > 0.6
def detect_file_type(filepath: Path) -> str:
"""ファイルを 'natural_language', 'code', 'config', 'unknown' に分類。"""
ext = filepath.suffix.lower()
if filepath.name.lower() in KNOWN_CODE_FILENAMES:
return "code"
if ext in COMPRESSIBLE_EXTENSIONS:
return "natural_language"
if ext in SKIP_EXTENSIONS:
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
if not ext:
try:
text = filepath.read_text(encoding="utf-8", errors="ignore")
except (OSError, PermissionError):
return "unknown"
lines = text.splitlines()[:50]
if text.startswith("#!"):
return "code"
if _is_json_content(text[:10000]):
return "config"
if _is_yaml_content(lines):
return "config"
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
non_empty = sum(1 for l in lines if l.strip())
if non_empty > 0 and code_lines / non_empty > 0.4:
return "code"
return "natural_language"
return "unknown"
def should_compress(filepath: Path) -> bool:
"""自然言語ファイルで圧縮対象なら True。"""
if not filepath.is_file():
return False
if filepath.name.endswith(".original.md"):
return False
return detect_file_type(filepath) == "natural_language"
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("使い方: python detect.py <file1> [file2] ...")
sys.exit(1)
for path_str in sys.argv[1:]:
p = Path(path_str).resolve()
file_type = detect_file_type(p)
compress = should_compress(p)
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
scripts/validate.py
#!/usr/bin/env python3
"""圧縮後ファイルの検証: 見出し・コードブロック・URL・パス・箇条書き保持確認。"""
import re
from collections import Counter
from pathlib import Path
URL_REGEX = re.compile(r"https?://[^\s)]+")
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
class ValidationResult:
def __init__(self):
self.is_valid = True
self.errors = []
self.warnings = []
def add_error(self, msg):
self.is_valid = False
self.errors.append(msg)
def add_warning(self, msg):
self.warnings.append(msg)
def read_file(path: Path) -> str:
return path.read_text(encoding="utf-8")
def extract_headings(text):
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
def extract_code_blocks(text):
"""行ベース fenced code block 抽出。
``` と ~~~ の可変長フェンス対応。CommonMark 準拠: 閉じフェンスは同文字・
開きフェンス以上の長さ必要。ネストフェンス(4バッククォート外側が3を包む等)対応。
"""
blocks = []
lines = text.split("\n")
i = 0
n = len(lines)
while i < n:
m = FENCE_OPEN_REGEX.match(lines[i])
if not m:
i += 1
continue
fence_char = m.group(2)[0]
fence_len = len(m.group(2))
open_line = lines[i]
block_lines = [open_line]
i += 1
closed = False
while i < n:
close_m = FENCE_OPEN_REGEX.match(lines[i])
if (
close_m
and close_m.group(2)[0] == fence_char
and len(close_m.group(2)) >= fence_len
and close_m.group(3).strip() == ""
):
block_lines.append(lines[i])
closed = True
i += 1
break
block_lines.append(lines[i])
i += 1
if closed:
blocks.append("\n".join(block_lines))
return blocks
def extract_urls(text):
return set(URL_REGEX.findall(text))
def extract_paths(text):
return set(PATH_REGEX.findall(text))
def count_bullets(text):
return len(BULLET_REGEX.findall(text))
def extract_inline_codes(text):
"""fenced code block を除外し、inline code を抽出。"""
text_without_fences = text
for block in extract_code_blocks(text):
text_without_fences = text_without_fences.replace(block, "", 1)
return re.findall(r"`([^`]+)`", text_without_fences)
def validate_headings(orig, comp, result):
h1 = extract_headings(orig)
h2 = extract_headings(comp)
if len(h1) != len(h2):
result.add_error(f"見出し数 不一致: {len(h1)} vs {len(h2)}")
if h1 != h2:
result.add_warning("見出しテキスト/順序 変更あり")
def validate_code_blocks(orig, comp, result):
c1 = extract_code_blocks(orig)
c2 = extract_code_blocks(comp)
if c1 != c2:
result.add_error("コードブロック 完全保持 失敗")
def validate_urls(orig, comp, result):
u1 = extract_urls(orig)
u2 = extract_urls(comp)
if u1 != u2:
result.add_error(f"URL 不一致: 消失={u1 - u2}, 追加={u2 - u1}")
def validate_paths(orig, comp, result):
p1 = extract_paths(orig)
p2 = extract_paths(comp)
if p1 != p2:
result.add_warning(f"パス 不一致: 消失={p1 - p2}, 追加={p2 - p1}")
def validate_bullets(orig, comp, result):
b1 = count_bullets(orig)
b2 = count_bullets(comp)
if b1 == 0:
return
diff = abs(b1 - b2) / b1
if diff > 0.15:
result.add_warning(f"箇条書き数 変化過大: {b1} -> {b2}")
def validate_inline_codes(orig, comp, result):
c1 = Counter(extract_inline_codes(orig))
c2 = Counter(extract_inline_codes(comp))
if c1 == c2:
return
lost = set(c1) - set(c2)
added = set(c2) - set(c1)
for code, count in c1.items():
if code in c2 and c2[code] < count:
lost.add(f"{code}({count}回中{count - c2[code]}回消失)")
if lost:
result.add_error(f"インラインコード消失: {lost}")
if added:
result.add_warning(f"インラインコード追加: {added}")
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
result = ValidationResult()
orig = read_file(original_path)
comp = read_file(compressed_path)
validate_headings(orig, comp, result)
validate_code_blocks(orig, comp, result)
validate_urls(orig, comp, result)
validate_paths(orig, comp, result)
validate_bullets(orig, comp, result)
validate_inline_codes(orig, comp, result)
return result
if __name__ == "__main__":
import sys
if len(sys.argv) != 3:
print("使い方: python validate.py <original> <compressed>")
sys.exit(1)
orig = Path(sys.argv[1]).resolve()
comp = Path(sys.argv[2]).resolve()
res = validate(orig, comp)
print(f"\n有効: {res.is_valid}")
if res.errors:
print("\nエラー:")
for e in res.errors:
print(f" - {e}")
if res.warnings:
print("\n警告:")
for w in res.warnings:
print(f" - {w}")
SKILL.md
---
name: genshijin-compress
description: >
自然言語メモリファイル(CLAUDE.md, todos, 設定)を原始人形式に圧縮し入力トークン削減。
技術内容・コード・URL・構造は完全保持。圧縮版が原ファイルを上書き、人間可読版は
FILE.original.md として保存。「/genshijin-compress <filepath>」「メモリファイル圧縮」で起動。
---
# genshijin-compress
## 目的
自然言語ファイル(CLAUDE.md, todos, 設定)を原始人モード化して入力トークン削減。圧縮版が原ファイル上書き、人間可読版は `<filename>.original.md` で保存。
## トリガー
`/genshijin-compress <filepath>` または「メモリファイル圧縮」等の依頼。
## 処理フロー
1. この SKILL.md と同ディレクトリの `scripts/` を検出
2. 実行:
```
cd <SKILL.md を含むディレクトリ> && python3 -m scripts <絶対ファイルパス>
```
3. CLI 処理内容:
- ファイル種別検出(トークン消費なし)
- Claude API/CLI で圧縮
- 出力検証(トークン消費なし)
- エラー時: Claude で該当箇所のみピンポイント修正(再圧縮せず)
- 最大2回リトライ
- 失敗時: ユーザーにエラー報告、原ファイルは無変更
4. 結果をユーザーに返却
## 圧縮ルール
### 削除
- 敬語・丁寧語(です/ます/ございます)
- クッション言葉(えーと/まあ/ちなみに/一応/とりあえず/基本的に/ざっくり言うと)
- 前置き(ご質問ありがとうございます/お力になれれば幸いです)
- ぼかし(〜かもしれません/〜と思われます/おそらく)
- 冗長表現(〜することができる→〜できる、〜というものは→〜は)
- 冗長接続(〜ということになりますので→だから、〜させていただく→する)
- 自明な助詞(が/の/を/に/で/は/と/も)
- 形容動詞活用語尾(な/に/で/だ)→ 語幹止め
- 形式名詞(こと/もの/ため)→ 名詞化 or 省略
- 補助動詞(ている/ておく/てしまう)→ 状態表現
### 完全保持(絶対変更しない)
- コードブロック(fenced ``` and indented)
- インラインコード(`backtick`)
- URL・リンク(完全URL、Markdownリンク)
- ファイルパス(`/src/components/...`, `./config.yaml`)
- コマンド(`npm install`, `git commit`, `docker build`)
- 技術用語(ライブラリ名、API名、プロトコル、アルゴリズム)
- 固有名詞(プロジェクト名、人名、企業名)
- 日付、バージョン番号、数値
- 環境変数(`$HOME`, `NODE_ENV`)
- エラーメッセージ原文
### 構造保持
- Markdown見出し全て(見出しテキストは完全保持、下の本文のみ圧縮)
- 箇条書き階層(ネストレベル維持)
- 番号付きリスト(番号維持)
- テーブル(構造維持、セル内テキストのみ圧縮)
- Frontmatter/YAMLヘッダ
### 圧縮
- 体言止め・用言止め
- 漢字連結で助詞吸収(「高負荷時に高速」→「高負荷時高速」)
- 和語→漢語化(「速く動作」→「高速動作」)
- 断片OK: 「コミット前テスト実行」不可「コミット前に必ずテストを実行してください」
- 「〜してください」「〜することを忘れずに」削除 → 動作のみ記述
- 重複箇条書きを統合
- 同パターン複数例は1例のみ残す
重要ルール:
``` ... ``` 内は **完全コピー**。
禁止:
- コメント削除
- スペース削除
- 行順変更
- コマンド短縮
- 簡略化一切
インラインコード(`...`)は完全保持。バッククォート内は一切変更しない。
コードブロック含むファイル:
- コードブロックは読取専用領域として扱う
- 外側のテキストのみ圧縮
- コード周辺セクションの結合禁止
## パターン例
### 例1
原文:
> main ブランチにプッシュする前には必ずテストスイートを実行するようにしてください。これはバグを早期に発見し、壊れたビルドが本番環境にデプロイされるのを防ぐために重要です。
圧縮:
> main push前テスト実行。バグ早期発見、壊れビルド本番デプロイ防止。
### 例2
原文:
> このアプリケーションは以下のコンポーネントを持つマイクロサービスアーキテクチャを採用しています。APIゲートウェイが全ての受信リクエストを処理し、適切なサービスにルーティングします。認証サービスはユーザーセッションとJWTトークンの管理を担当します。
圧縮:
> マイクロサービスアーキテクチャ。APIゲートウェイ 全受信リクエストをサービスにルーティング。認証サービス ユーザーセッション + JWTトークン管理。
## 境界
- 自然言語ファイルのみ圧縮(.md, .txt, 拡張子なし)
- 絶対変更しない: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
- 混在ファイル(散文 + コード)は散文部分のみ圧縮
- コードか散文か判断つかない場合は変更しない
- 原ファイルは FILE.original.md にバックアップしてから上書き
- FILE.original.md は絶対圧縮しない(スキップ)
- 機密ファイル(.env, credentials, id_rsa 等)は絶対に送信しない → 拒否
## セキュリティ
圧縮は Anthropic API に生データ送信。以下は拒否:
- `.env`, `.netrc`
- `credentials.*`, `secrets.*`, `passwords.*`
- SSH鍵(`id_rsa`, `id_ed25519` 等)
- 証明書(`.pem`, `.key`, `.p12`, `.crt` 等)
- ディレクトリ `.ssh`, `.aws`, `.gnupg`, `.kube`, `.docker` 配下
- ファイル名に `secret`/`credential`/`password`/`apikey`/`token`/`privatekey` 含むもの
ヒューリスティック誤検知時は、ファイル名変更で回避可能。