feat(our-gdrive-organizer): add new skill at slot 82, rename old 82 → 92
New Python CLI + dual SKILL.md (Code + Desktop) for organizing Google Drive folders under OurDigital conventions: - Refresh root README index (preserves manual Topics/Notes between AUTO-STRUCTURE markers) - Ensure per-subfolder README.md meta files - Propose filename + folder renames (D.intelligence → OurDigital with SEO-context caveat documented in patterns/gotchas.md) - Propose moves for cluttered files (screenshots, temp downloads) - Sensitive-folder skip list (04_Case Studies, 99_Project Archive, *Archive*, 진단*) - shared/patterns/ gotcha library: canonical-files, canonical-folders, categorization-rules, 12 known gotchas — grows over time as the system encounters new edge cases Slash command: /organize. CLI: ~/.local/bin/our-gdrive-organize. 82-tui-design-template renumbered to 92 (no content change) to free slot 82. AGENTS.md and CLAUDE.md updated for both moves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -195,10 +195,17 @@ Task(
|
|||||||
- **Always ask user consent** before executing any cleanup or system changes
|
- **Always ask user consent** before executing any cleanup or system changes
|
||||||
- Scripts are in `custom-skills/81-mac-optimizer/scripts/`
|
- Scripts are in `custom-skills/81-mac-optimizer/scripts/`
|
||||||
- Reference docs in `custom-skills/81-mac-optimizer/references/`
|
- Reference docs in `custom-skills/81-mac-optimizer/references/`
|
||||||
- **82-tui-design-template**: TUI wizard interface design (Norton Commander / Gopher style, Rich)
|
- **82-our-gdrive-organizer**: Organize a Google Drive folder under OurDigital conventions
|
||||||
|
- Refreshes root `README.md` index (Snapshot + Structure section between AUTO-STRUCTURE markers; preserves manual Topics/Notes)
|
||||||
|
- Ensures per-subfolder `README.md` meta files with auto-indexed Contents block
|
||||||
|
- Proposes filename renames (`D.intelligence` → `OurDigital`) and moves (screenshots, temp downloads → tidy subfolders)
|
||||||
|
- Skips sensitive folders by default: `04_Case Studies/`, `99_Project Archive/`, `*Archive$`, `진단*`
|
||||||
|
- Pure Python stdlib CLI: `~/.local/bin/our-gdrive-organize [TARGET] [--apply] [--scope full|index|subreadmes|rename|move]`
|
||||||
|
- Slash command: `/organize`
|
||||||
|
|
||||||
### Reference Curator Skills (90-91)
|
### Reference Curator Skills (90-91) & Design Templates (92)
|
||||||
|
|
||||||
|
- **92-tui-design-template**: TUI wizard interface design (Norton Commander / Gopher style, Rich) — moved from slot 82 to make room for `our-gdrive-organizer`
|
||||||
- Use **reference-curator-pipeline** for full automated curation workflows
|
- Use **reference-curator-pipeline** for full automated curation workflows
|
||||||
- Runs as background task, coordinates all 6 skills in sequence
|
- Runs as background task, coordinates all 6 skills in sequence
|
||||||
- Handles QA loops automatically (max 3 refactor, 2 deep_research iterations)
|
- Handles QA loops automatically (max 3 refactor, 2 deep_research iterations)
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ This is a Claude Skills collection repository containing:
|
|||||||
|---|-------|---------|---------|
|
|---|-------|---------|---------|
|
||||||
| 80 | claude-settings-optimizer | Claude settings optimization & token audit | "settings audit", "exceed response limit", "MCP error" |
|
| 80 | claude-settings-optimizer | Claude settings optimization & token audit | "settings audit", "exceed response limit", "MCP error" |
|
||||||
| 81 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" |
|
| 81 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" |
|
||||||
| 82 | tui-design-template | TUI wizard interface design (Norton Commander / Gopher style, Rich) | "build TUI", "CLI wizard", "terminal UI", "Rich TUI" |
|
| 82 | our-gdrive-organizer | Organize a Google Drive folder under OurDigital conventions: refresh README index, refresh per-subfolder READMEs, propose renames + moves | "/organize", "organize the Drive folder", "refresh the index", "rescan the folder" |
|
||||||
|
|
||||||
## Dual-Platform Skill Structure
|
## Dual-Platform Skill Structure
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ our-claude-skills/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── 80-claude-settings-optimizer/
|
│ ├── 80-claude-settings-optimizer/
|
||||||
│ ├── 81-mac-optimizer/
|
│ ├── 81-mac-optimizer/
|
||||||
│ ├── 82-tui-design-template/
|
│ ├── 82-our-gdrive-organizer/
|
||||||
│ │
|
│ │
|
||||||
│ ├── 90-reference-curator/ # Modular reference documentation suite
|
│ ├── 90-reference-curator/ # Modular reference documentation suite
|
||||||
│ │ ├── 01-reference-discovery/
|
│ │ ├── 01-reference-discovery/
|
||||||
@@ -275,7 +275,9 @@ our-claude-skills/
|
|||||||
│ │ ├── shared/
|
│ │ ├── shared/
|
||||||
│ │ └── install.sh
|
│ │ └── install.sh
|
||||||
│ │
|
│ │
|
||||||
│ └── 91-multi-agent-guide/
|
│ ├── 91-multi-agent-guide/
|
||||||
|
│ │
|
||||||
|
│ └── 92-tui-design-template/
|
||||||
│
|
│
|
||||||
├── example-skills/skills-main/
|
├── example-skills/skills-main/
|
||||||
├── official-skills/
|
├── official-skills/
|
||||||
|
|||||||
96
custom-skills/82-our-gdrive-organizer/README.md
Normal file
96
custom-skills/82-our-gdrive-organizer/README.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# our-gdrive-organizer
|
||||||
|
|
||||||
|
CLI + Claude skill for organizing OurDigital Google Drive folders.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Generalize the README-index-refresh pattern (originally built for
|
||||||
|
`02_SEO in Action/`) so it works on any 2nd-level subfolder of the user's
|
||||||
|
Google Drive Stream. Adds:
|
||||||
|
|
||||||
|
- Per-subfolder `README.md` meta files (with AUTO-STRUCTURE blocks)
|
||||||
|
- Proposed filename normalization (`D.intelligence` → `OurDigital`)
|
||||||
|
- Proposed file moves for cluttered roots (screenshots, temp downloads)
|
||||||
|
- Sensitive-folder skip list so client archives are never touched
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
|
||||||
|
- **As a Claude Code skill**: triggered by phrases like "organize the Drive
|
||||||
|
folder", "refresh the index", "/organize", or "/our-gdrive-organizer".
|
||||||
|
- **As a CLI tool**: run `our-gdrive-organize [TARGET]` from any terminal.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
our-gdrive-organizer/
|
||||||
|
├── code/
|
||||||
|
│ ├── SKILL.md # Claude Code skill definition
|
||||||
|
│ └── organizer.py # Main Python CLI (stdlib only)
|
||||||
|
├── desktop/
|
||||||
|
│ └── SKILL.md # Claude Desktop skill definition
|
||||||
|
├── shared/
|
||||||
|
│ └── conventions.md # Canonical naming convention spec
|
||||||
|
├── docs/
|
||||||
|
│ └── CHANGELOG.md
|
||||||
|
└── README.md # ← you are here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
The repository expects two symlinks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skill discovery for Claude Code
|
||||||
|
ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code \
|
||||||
|
~/.claude/skills/our-gdrive-organizer
|
||||||
|
|
||||||
|
# CLI on PATH
|
||||||
|
ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py \
|
||||||
|
~/.local/bin/our-gdrive-organize
|
||||||
|
```
|
||||||
|
|
||||||
|
A slash-command alias lives at `~/.claude/commands/organize.md`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dry-run report (default — never modifies files except writing the README)
|
||||||
|
our-gdrive-organize ~/Library/CloudStorage/GoogleDrive-*/My\ Drive/02_SEO\ in\ Action
|
||||||
|
|
||||||
|
# Apply all proposed renames + moves
|
||||||
|
our-gdrive-organize <TARGET> --apply
|
||||||
|
|
||||||
|
# Only refresh the root README index
|
||||||
|
our-gdrive-organize <TARGET> --scope index
|
||||||
|
|
||||||
|
# Only ensure each top-level subfolder has a README with AUTO block
|
||||||
|
our-gdrive-organize <TARGET> --scope subreadmes
|
||||||
|
|
||||||
|
# Only propose renames (still requires --apply to execute)
|
||||||
|
our-gdrive-organize <TARGET> --scope rename
|
||||||
|
|
||||||
|
# Machine-readable output
|
||||||
|
our-gdrive-organize <TARGET> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
See `shared/conventions.md` for the full naming and structure spec. The
|
||||||
|
script's rules (`RENAME_RULES`, `MOVE_RULES`, `SENSITIVE_SUBFOLDER_PATTERNS`)
|
||||||
|
in `code/organizer.py` are the source of truth for what's actually enforced;
|
||||||
|
keep them in sync with `conventions.md`.
|
||||||
|
|
||||||
|
## What it never touches
|
||||||
|
|
||||||
|
- Files inside `04_Case Studies/`, `99_Project Archive/`, any `*Archive*`
|
||||||
|
folder, or any folder starting with `진단` (real client material).
|
||||||
|
- The manually-curated Topics / Notes sections of the root README.
|
||||||
|
- Cell content of `.gsheet` / body of `.gdoc` / slides of `.gslides` —
|
||||||
|
filesystem renames only change the local stub filename.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.9+ (uses `from __future__ import annotations`, dataclasses, pathlib)
|
||||||
|
- macOS (paths assume Google Drive Cloud Storage at
|
||||||
|
`~/Library/CloudStorage/GoogleDrive-*/My Drive/`); easily adapted for other
|
||||||
|
OSes by changing the target path.
|
||||||
209
custom-skills/82-our-gdrive-organizer/code/SKILL.md
Normal file
209
custom-skills/82-our-gdrive-organizer/code/SKILL.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
name: our-gdrive-organizer
|
||||||
|
description: |
|
||||||
|
Organize a Google Drive folder under OurDigital conventions: refresh root
|
||||||
|
README.md index, refresh per-subfolder README.md meta files, propose renames
|
||||||
|
for files using the old D.intelligence brand, and propose moves for cluttered
|
||||||
|
files (screenshots at the wrong level, temp/partial downloads).
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
- "organize the Drive folder", "organize this folder"
|
||||||
|
- "refresh the index", "rescan the folder", "update README"
|
||||||
|
- "clean up cluttered files", "propose renames"
|
||||||
|
- "/organize", "/organize-drive", "/our-gdrive-organizer"
|
||||||
|
|
||||||
|
Default target is the current working directory. Generalized to work on any
|
||||||
|
2nd-level subfolder of the user's Google Drive Stream (My Drive/00_..., 01_...,
|
||||||
|
02_..., etc.) — not specific to one folder.
|
||||||
|
version: "1.0"
|
||||||
|
author: OurDigital
|
||||||
|
environment: Code
|
||||||
|
---
|
||||||
|
|
||||||
|
# our-gdrive-organizer (Code)
|
||||||
|
|
||||||
|
Walks a target directory (3 levels deep), refreshes the index README, proposes
|
||||||
|
renames + moves under OurDigital naming conventions, and optionally applies.
|
||||||
|
|
||||||
|
Source of truth for the conventions: `../shared/conventions.md`.
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
|
||||||
|
The user wants to organize one of their Drive folders. Cues:
|
||||||
|
- An explicit ask: "organize", "refresh index", "rename per convention", "scan for changes"
|
||||||
|
- The user is sitting inside a 2nd-level Drive folder (`~/Library/CloudStorage/GoogleDrive-*/My Drive/NN_*/`)
|
||||||
|
- Slash invocation: `/organize`, `/our-gdrive-organizer`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1 — Run dry-run, then summarize
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET"
|
||||||
|
```
|
||||||
|
|
||||||
|
(`$TARGET` defaults to cwd; pass an absolute path for a specific folder.)
|
||||||
|
|
||||||
|
The script writes the README index immediately (idempotent — skips when no
|
||||||
|
structural change), but never renames or moves files without `--apply`.
|
||||||
|
|
||||||
|
Read the report and tell the user, in plain language:
|
||||||
|
- How many structural changes were folded into the README
|
||||||
|
- Each rename proposal (old → new, reason)
|
||||||
|
- Each move proposal (file → destination subfolder, reason)
|
||||||
|
- Subfolders that were skipped because they're "sensitive" (`04_Case Studies`,
|
||||||
|
`99_Project Archive`, `*Archive*`, `진단*`)
|
||||||
|
|
||||||
|
### Step 2 — Confirm with user before applying
|
||||||
|
|
||||||
|
If the user says go ahead (or "apply", "yes", "do it"):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET" --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user wants only part of the work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET" --scope rename --apply
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET" --scope move --apply
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET" --scope index # always writes
|
||||||
|
~/.local/bin/our-gdrive-organize "$TARGET" --scope subreadmes # always writes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3 — Verify
|
||||||
|
|
||||||
|
After applying, run the dry-run once more and confirm the proposal list is
|
||||||
|
empty (or only contains items the user explicitly skipped).
|
||||||
|
|
||||||
|
## Important guardrails
|
||||||
|
|
||||||
|
- **Never rename or move files inside `04_Case Studies/`, `99_Project Archive/`,
|
||||||
|
any `*Archive*` folder, or any folder starting with `진단`.** Those contain
|
||||||
|
real client engagement records that must keep their original filenames.
|
||||||
|
- The script's rename/move rules live in `code/organizer.py` near the top of
|
||||||
|
the file (`RENAME_RULES`, `MOVE_RULES`, `SENSITIVE_SUBFOLDER_PATTERNS`). If
|
||||||
|
the user asks to add or change a rule, edit there and re-run.
|
||||||
|
- Filename renames on `.gsheet` / `.gdoc` stub files only change the local
|
||||||
|
filename — the actual Google Drive document and its sharing links are
|
||||||
|
preserved (the stub holds a Doc ID, not the content).
|
||||||
|
|
||||||
|
## What the script does NOT do
|
||||||
|
|
||||||
|
- Does not edit cell content inside `.gsheet` / `.gdoc` / `.xlsx` / `.pdf` —
|
||||||
|
only local-filesystem renames. Cell-level neutralization stays a manual task.
|
||||||
|
- Does not delete anything.
|
||||||
|
- Does not modify the manually-curated Topics / Notes sections of the root
|
||||||
|
README — only the AUTO-STRUCTURE block.
|
||||||
|
- Does not categorize files by reading their **content** — for that, use the
|
||||||
|
Content-based reorganization workflow below.
|
||||||
|
|
||||||
|
## Folder-rename support
|
||||||
|
|
||||||
|
The script proposes folder renames (in addition to file renames) using the
|
||||||
|
same `RENAME_RULES`. Guardrails:
|
||||||
|
- **Top-level subfolders are NEVER renamed automatically.** Names like
|
||||||
|
`00_Brand Management/` or `02_SEO Audit Toolkit/` are user-curated
|
||||||
|
practice areas. If the user wants one renamed, do it as a one-off `mv`.
|
||||||
|
- Sensitive folders are skipped entirely (not renamed, not recursed into).
|
||||||
|
- Eligible folders: depth-2 and deeper (e.g.,
|
||||||
|
`00_Brand Management/D.intelligence SEO Audit/` → `…/OurDigital SEO Audit/`).
|
||||||
|
|
||||||
|
When `--scope rename --apply` runs, file renames execute first, then folder
|
||||||
|
renames — order matters because renaming a folder first would invalidate
|
||||||
|
the file rename paths inside it.
|
||||||
|
|
||||||
|
## Content-based reorganization (interactive)
|
||||||
|
|
||||||
|
The script handles deterministic naming-pattern work. For judgment calls
|
||||||
|
(which folder does this file truly belong in?), use this workflow.
|
||||||
|
|
||||||
|
### When to use
|
||||||
|
|
||||||
|
The user says any of:
|
||||||
|
- "look at the contents and reorganize"
|
||||||
|
- "this folder feels cluttered, suggest a better layout"
|
||||||
|
- "categorize the files in {subfolder}"
|
||||||
|
- "audit my folder structure"
|
||||||
|
|
||||||
|
Or you notice during a regular `/organize` run that:
|
||||||
|
- A subfolder root has many loose files that should plausibly be grouped
|
||||||
|
- Files appear duplicated across subfolders
|
||||||
|
- Filenames hint at content that doesn't match their location
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. **Anchor yourself** — read the patterns library before proposing anything:
|
||||||
|
- `../shared/patterns/canonical-folders.md` — what well-organized shapes look like
|
||||||
|
- `../shared/patterns/canonical-files.md` — what well-named files look like
|
||||||
|
- `../shared/patterns/categorization-rules.md` — IF→THEN placement rules
|
||||||
|
- `../shared/patterns/gotchas.md` — known edge cases
|
||||||
|
|
||||||
|
These are the gotcha library. Re-read them every session — they grow over time.
|
||||||
|
|
||||||
|
2. **Pick one subfolder at a time.** Don't try to reorganize an entire
|
||||||
|
2nd-level folder in one pass — too much for the user to review.
|
||||||
|
|
||||||
|
3. **Sample file contents.** For each file in the chosen subfolder:
|
||||||
|
- Markdown / txt / json: `Read` directly.
|
||||||
|
- `.gsheet` / `.gdoc` stubs: read the JSON to extract the doc_id, but
|
||||||
|
accept that you can't see actual cell content. Use the FILENAME
|
||||||
|
pattern + adjacent context.
|
||||||
|
- PDFs / .docx / .pptx / .xlsx: you can't read content with stdlib.
|
||||||
|
Either ask the user to summarize, or skip and rely on filename.
|
||||||
|
- For each file, note 1–2 sentences: what's it about, where would it
|
||||||
|
belong by content?
|
||||||
|
|
||||||
|
4. **Build proposals**, grouped by destination:
|
||||||
|
```
|
||||||
|
Move from `02_SEO Audit Toolkit/` → `04_Case Studies/`:
|
||||||
|
- `signed-acme-contract.pdf` (real client name + signed contract content)
|
||||||
|
|
||||||
|
Move from `02_SEO Audit Toolkit/` → `05_Working Template/`:
|
||||||
|
- `OurDigital-OOO Audit Template.gsheet` (placeholder name → template)
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Present one batch (one source folder) at a time.** Ask:
|
||||||
|
"Should I apply these N moves from `{source}/`? Yes / No / partial (which)."
|
||||||
|
|
||||||
|
6. **Apply via `Bash mv`** for each confirmed move:
|
||||||
|
```bash
|
||||||
|
mv "/path/to/source/file" "/path/to/destination/file"
|
||||||
|
```
|
||||||
|
Then refresh the index:
|
||||||
|
```bash
|
||||||
|
our-gdrive-organize "$TARGET" --scope index
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Capture new gotchas.** If you encountered an ambiguous case the
|
||||||
|
patterns library didn't cover, append it to
|
||||||
|
`../shared/patterns/gotchas.md` before ending the session — that's how
|
||||||
|
the system learns.
|
||||||
|
|
||||||
|
### Sensitive-folder reminder
|
||||||
|
|
||||||
|
When proposing moves in content-based mode, the same guardrails apply:
|
||||||
|
**never propose moving files INTO or OUT OF**:
|
||||||
|
- `04_Case Studies/`
|
||||||
|
- `99_Project Archive/`
|
||||||
|
- Any `*Archive*` folder
|
||||||
|
- Any folder starting with `진단`
|
||||||
|
|
||||||
|
If you notice something in those folders that looks misplaced, flag it to
|
||||||
|
the user as a manual review item — don't propose an automated move.
|
||||||
|
|
||||||
|
## When the user asks to extend
|
||||||
|
|
||||||
|
To add a new rename rule, edit `RENAME_RULES` in `code/organizer.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(re.compile(r"oldpattern", re.I), "newpattern", "human-readable reason"),
|
||||||
|
```
|
||||||
|
|
||||||
|
To add a new move rule, edit `MOVE_RULES`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
(re.compile(r"^pattern\.ext$"), "destination_subfolder", "reason"),
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `../shared/conventions.md` whenever rules change.
|
||||||
520
custom-skills/82-our-gdrive-organizer/code/organizer.py
Executable file
520
custom-skills/82-our-gdrive-organizer/code/organizer.py
Executable file
@@ -0,0 +1,520 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
our-gdrive-organizer — organize a Google Drive folder under OurDigital conventions.
|
||||||
|
|
||||||
|
Scans the target directory (default: 2nd-level Drive subfolder), refreshes the
|
||||||
|
README.md index, proposes renames + moves to bring files into convention, and
|
||||||
|
optionally applies them.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
organizer.py [TARGET] # dry-run report
|
||||||
|
organizer.py [TARGET] --apply # apply all proposed changes
|
||||||
|
organizer.py [TARGET] --scope index # only refresh root README
|
||||||
|
organizer.py [TARGET] --scope subreadmes # refresh per-subfolder READMEs
|
||||||
|
organizer.py [TARGET] --scope rename # only propose/apply renames
|
||||||
|
organizer.py [TARGET] --scope move # only propose/apply moves
|
||||||
|
organizer.py [TARGET] --scope full # everything (default)
|
||||||
|
organizer.py [TARGET] --json # machine-readable output
|
||||||
|
|
||||||
|
If TARGET is omitted, uses the current working directory.
|
||||||
|
|
||||||
|
The script is stdlib-only (Python 3.9+). Safe by default — never writes unless
|
||||||
|
--apply is passed (or running --scope index, which always writes the README).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass, field, asdict
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration: naming convention enforcement.
|
||||||
|
# Edit shared/conventions.md for the human-readable spec; this dict is the
|
||||||
|
# source of truth for the script.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
RENAME_RULES: list[tuple[re.Pattern[str], str, str]] = [
|
||||||
|
# (pattern, replacement, reason)
|
||||||
|
# SEO context only — D.intelligence is the parent company; OurDigital is its
|
||||||
|
# SEO-specialty child brand. Only apply these renames when operating inside
|
||||||
|
# an SEO folder (02_SEO in Action, 04_SEO, etc.). Outside SEO context, keep
|
||||||
|
# D.intelligence names intact (they belong to the parent company). The
|
||||||
|
# script doesn't enforce this caveat — it relies on the caller pointing at
|
||||||
|
# the right target. See shared/patterns/gotchas.md "D.intelligence vs
|
||||||
|
# OurDigital" for the full rule.
|
||||||
|
(re.compile(r"D\.intelligence Lab-", re.I), "OurDigital-",
|
||||||
|
"parent-co SEO asset → OurDigital-"),
|
||||||
|
(re.compile(r"D\.intelligence", re.I), "OurDigital",
|
||||||
|
"parent-co SEO asset → OurDigital"),
|
||||||
|
# Variant without the dot (typo / informal spelling).
|
||||||
|
(re.compile(r"\bD intelligence\b", re.I), "OurDigital",
|
||||||
|
"parent-co SEO asset (no-dot variant) → OurDigital"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Files matching these patterns at the root of a top-level subfolder are
|
||||||
|
# proposed to be moved into a sibling subfolder.
|
||||||
|
MOVE_RULES: list[tuple[re.Pattern[str], str, str]] = [
|
||||||
|
(re.compile(r"^Screenshot \d{4}-\d{2}-\d{2}.*\.png$"), "screenshots",
|
||||||
|
"screenshot file → screenshots/ subfolder"),
|
||||||
|
(re.compile(r"^.*\.(crdownload|tmp|partial)$"), "_unsorted",
|
||||||
|
"incomplete / temp file → _unsorted/"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Skip these subfolders for invasive changes (renames + moves).
|
||||||
|
# Indexing still includes them, but their internal files aren't reorganized.
|
||||||
|
SENSITIVE_SUBFOLDER_PATTERNS = [
|
||||||
|
re.compile(r"^04_Case Studies$"),
|
||||||
|
re.compile(r"^99_Project Archive$"),
|
||||||
|
re.compile(r".*Archive$", re.I),
|
||||||
|
re.compile(r"^진단.*$"),
|
||||||
|
]
|
||||||
|
|
||||||
|
EXCLUDE_NAMES = {".DS_Store", "Icon\r"}
|
||||||
|
|
||||||
|
BEGIN_MARK = "<!-- AUTO-STRUCTURE:BEGIN -->"
|
||||||
|
END_MARK = "<!-- AUTO-STRUCTURE:END -->"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data classes
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FolderScan:
|
||||||
|
path: Path
|
||||||
|
files: list[str] = field(default_factory=list)
|
||||||
|
subfolders: dict[str, "FolderScan"] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Proposal:
|
||||||
|
kind: str # "rename" | "move"
|
||||||
|
src: Path
|
||||||
|
dst: Path
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
def as_dict(self) -> dict:
|
||||||
|
return {"kind": self.kind, "src": str(self.src),
|
||||||
|
"dst": str(self.dst), "reason": self.reason}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scanning
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def is_visible(name: str) -> bool:
|
||||||
|
return not name.startswith(".") and name not in EXCLUDE_NAMES
|
||||||
|
|
||||||
|
|
||||||
|
def list_visible_files(path: Path) -> list[str]:
|
||||||
|
try:
|
||||||
|
return sorted(e.name for e in os.scandir(path)
|
||||||
|
if e.is_file(follow_symlinks=False) and is_visible(e.name))
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def list_visible_dirs(path: Path) -> list[str]:
|
||||||
|
try:
|
||||||
|
return sorted(e.name for e in os.scandir(path)
|
||||||
|
if e.is_dir(follow_symlinks=False) and is_visible(e.name))
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def scan(path: Path, depth: int = 3) -> FolderScan:
|
||||||
|
"""Walk depth levels deep. depth=3 → root + 2 levels of subfolders."""
|
||||||
|
s = FolderScan(path=path, files=list_visible_files(path))
|
||||||
|
if depth > 1:
|
||||||
|
for d in list_visible_dirs(path):
|
||||||
|
s.subfolders[d] = scan(path / d, depth - 1)
|
||||||
|
else:
|
||||||
|
for d in list_visible_dirs(path):
|
||||||
|
s.subfolders[d] = FolderScan(path=path / d)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def is_sensitive(name: str) -> bool:
|
||||||
|
return any(p.search(name) for p in SENSITIVE_SUBFOLDER_PATTERNS)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Index generation (root README.md and per-subfolder READMEs)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def render_structure_block(scan_root: FolderScan) -> str:
|
||||||
|
out: list[str] = []
|
||||||
|
out.append("## Structure (3 levels)")
|
||||||
|
out.append("")
|
||||||
|
ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M %z")
|
||||||
|
out.append(f"_Auto-regenerated {ts}. Edits inside the AUTO-STRUCTURE markers will be overwritten._")
|
||||||
|
out.append("")
|
||||||
|
for d1_name in sorted(scan_root.subfolders):
|
||||||
|
d1 = scan_root.subfolders[d1_name]
|
||||||
|
nf1 = len(d1.files)
|
||||||
|
nd1 = len(d1.subfolders)
|
||||||
|
out.append(f"- **{d1_name}/** ({nf1} files, {nd1} subfolders)")
|
||||||
|
for d2_name in sorted(d1.subfolders):
|
||||||
|
d2 = d1.subfolders[d2_name]
|
||||||
|
nf2 = len(d2.files)
|
||||||
|
nd2 = len(d2.subfolders)
|
||||||
|
if nd2 > 0:
|
||||||
|
out.append(f" - {d2_name}/ ({nf2}f, {nd2}d)")
|
||||||
|
for d3_name in sorted(d2.subfolders):
|
||||||
|
d3 = d2.subfolders[d3_name]
|
||||||
|
nf3 = len(d3.files)
|
||||||
|
out.append(f" - {d3_name}/ ({nf3}f)")
|
||||||
|
else:
|
||||||
|
out.append(f" - {d2_name}/ ({nf2}f)")
|
||||||
|
if nf1 > 0:
|
||||||
|
preview = ", ".join(f"`{n}`" for n in d1.files[:5])
|
||||||
|
out.append(f" - _files_: {preview}")
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out).rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def render_subfolder_block(scan_node: FolderScan) -> str:
|
||||||
|
"""Smaller block for per-subfolder README.md (2 levels deep)."""
|
||||||
|
out: list[str] = []
|
||||||
|
out.append("## Contents")
|
||||||
|
out.append("")
|
||||||
|
ts = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M %z")
|
||||||
|
out.append(f"_Auto-regenerated {ts}._")
|
||||||
|
out.append("")
|
||||||
|
if scan_node.files:
|
||||||
|
out.append(f"**Files at root** ({len(scan_node.files)}):")
|
||||||
|
for f in scan_node.files[:20]:
|
||||||
|
out.append(f"- `{f}`")
|
||||||
|
if len(scan_node.files) > 20:
|
||||||
|
out.append(f"- … and {len(scan_node.files) - 20} more")
|
||||||
|
out.append("")
|
||||||
|
if scan_node.subfolders:
|
||||||
|
out.append(f"**Subfolders** ({len(scan_node.subfolders)}):")
|
||||||
|
for d_name in sorted(scan_node.subfolders):
|
||||||
|
d = scan_node.subfolders[d_name]
|
||||||
|
nf = len(d.files)
|
||||||
|
nd = len(d.subfolders)
|
||||||
|
extras = f", {nd} dirs" if nd else ""
|
||||||
|
out.append(f"- `{d_name}/` ({nf} files{extras})")
|
||||||
|
out.append("")
|
||||||
|
return "\n".join(out).rstrip() + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def splice_readme(readme_path: Path, new_block: str, *, header_default: str | None = None) -> tuple[bool, str]:
|
||||||
|
"""Insert/replace the AUTO-STRUCTURE block inside readme_path.
|
||||||
|
|
||||||
|
Returns (changed, summary). Creates the file with header_default if missing.
|
||||||
|
"""
|
||||||
|
if not readme_path.exists():
|
||||||
|
if header_default is None:
|
||||||
|
return (False, f"skip: {readme_path} does not exist")
|
||||||
|
readme_path.write_text(
|
||||||
|
f"{header_default}\n\n{BEGIN_MARK}\n{new_block}{END_MARK}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return (True, f"created: {readme_path}")
|
||||||
|
|
||||||
|
text = readme_path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
# Update top-level Snapshot counts if they exist
|
||||||
|
parent = readme_path.parent
|
||||||
|
total_files = len(list_visible_files(parent))
|
||||||
|
total_dirs = len(list_visible_dirs(parent))
|
||||||
|
text = re.sub(r"^- \*\*Top-level files\*\*:.*$",
|
||||||
|
f"- **Top-level files**: {total_files}", text, count=1, flags=re.M)
|
||||||
|
text = re.sub(r"^- \*\*Top-level subfolders\*\*:.*$",
|
||||||
|
f"- **Top-level subfolders**: {total_dirs}", text, count=1, flags=re.M)
|
||||||
|
|
||||||
|
if BEGIN_MARK in text and END_MARK in text:
|
||||||
|
pattern = re.compile(re.escape(BEGIN_MARK) + r".*?" + re.escape(END_MARK), re.S)
|
||||||
|
new_text = pattern.sub(BEGIN_MARK + "\n" + new_block + END_MARK, text)
|
||||||
|
else:
|
||||||
|
# Append marker block before the trailing footer (if any) or at EOF
|
||||||
|
new_text = text.rstrip() + "\n\n" + BEGIN_MARK + "\n" + new_block + END_MARK + "\n"
|
||||||
|
|
||||||
|
if structurally_equal(text, new_text):
|
||||||
|
return (False, f"no structural changes: {readme_path}")
|
||||||
|
readme_path.write_text(new_text, encoding="utf-8")
|
||||||
|
return (True, f"updated: {readme_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def structurally_equal(a: str, b: str) -> bool:
|
||||||
|
"""Strip the auto-regenerated timestamp lines before comparing."""
|
||||||
|
strip = re.compile(r"^_Auto-regenerated .*$", re.M)
|
||||||
|
return strip.sub("", a) == strip.sub("", b)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Rename + move proposals
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def propose_renames(scan_root: FolderScan) -> list[Proposal]:
|
||||||
|
proposals: list[Proposal] = []
|
||||||
|
def visit(node: FolderScan):
|
||||||
|
for f in node.files:
|
||||||
|
new = f
|
||||||
|
reasons: list[str] = []
|
||||||
|
for pat, repl, why in RENAME_RULES:
|
||||||
|
next_new = pat.sub(repl, new)
|
||||||
|
if next_new != new:
|
||||||
|
reasons.append(why)
|
||||||
|
new = next_new
|
||||||
|
if new != f:
|
||||||
|
proposals.append(Proposal("rename", node.path / f,
|
||||||
|
node.path / new, "; ".join(reasons)))
|
||||||
|
for sub_name, sub in node.subfolders.items():
|
||||||
|
if is_sensitive(sub_name):
|
||||||
|
continue
|
||||||
|
visit(sub)
|
||||||
|
visit(scan_root)
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
|
def propose_folder_renames(scan_root: FolderScan) -> list[Proposal]:
|
||||||
|
"""Propose folder renames matching RENAME_RULES.
|
||||||
|
|
||||||
|
Guardrails:
|
||||||
|
- Top-level subfolders (depth 1, e.g. "00_Brand Management/") are NEVER
|
||||||
|
renamed automatically — they're the user's manually curated practice
|
||||||
|
areas. Use a one-off `mv` if you need to rename one.
|
||||||
|
- Sensitive folders are skipped entirely (not renamed, not recursed into).
|
||||||
|
"""
|
||||||
|
proposals: list[Proposal] = []
|
||||||
|
|
||||||
|
def visit(node: FolderScan, parent_depth: int):
|
||||||
|
sub_depth = parent_depth + 1
|
||||||
|
for sub_name, sub in node.subfolders.items():
|
||||||
|
if is_sensitive(sub_name):
|
||||||
|
continue
|
||||||
|
if sub_depth >= 2: # depth-2+ folders are eligible for rename
|
||||||
|
new_name = sub_name
|
||||||
|
reasons: list[str] = []
|
||||||
|
for pat, repl, why in RENAME_RULES:
|
||||||
|
nxt = pat.sub(repl, new_name)
|
||||||
|
if nxt != new_name:
|
||||||
|
reasons.append(why)
|
||||||
|
new_name = nxt
|
||||||
|
if new_name != sub_name:
|
||||||
|
proposals.append(Proposal(
|
||||||
|
"rename-folder", sub.path,
|
||||||
|
sub.path.parent / new_name,
|
||||||
|
"; ".join(reasons),
|
||||||
|
))
|
||||||
|
visit(sub, sub_depth)
|
||||||
|
|
||||||
|
visit(scan_root, parent_depth=0)
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
|
def propose_moves(scan_root: FolderScan) -> list[Proposal]:
|
||||||
|
proposals: list[Proposal] = []
|
||||||
|
# Only operate on top-level subfolders of the target — that's where the
|
||||||
|
# "cluttered files at root" pattern most commonly appears.
|
||||||
|
for sub_name, sub in scan_root.subfolders.items():
|
||||||
|
if is_sensitive(sub_name):
|
||||||
|
continue
|
||||||
|
for f in sub.files:
|
||||||
|
for pat, dest_subdir, why in MOVE_RULES:
|
||||||
|
if pat.search(f):
|
||||||
|
proposals.append(Proposal(
|
||||||
|
"move",
|
||||||
|
sub.path / f,
|
||||||
|
sub.path / dest_subdir / f,
|
||||||
|
why,
|
||||||
|
))
|
||||||
|
break
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
|
||||||
|
def apply_proposals(props: Iterable[Proposal]) -> list[tuple[Proposal, str]]:
|
||||||
|
results: list[tuple[Proposal, str]] = []
|
||||||
|
for p in props:
|
||||||
|
try:
|
||||||
|
if p.kind == "move":
|
||||||
|
p.dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if p.dst.exists():
|
||||||
|
results.append((p, "skip: destination exists"))
|
||||||
|
continue
|
||||||
|
shutil.move(str(p.src), str(p.dst))
|
||||||
|
results.append((p, "ok"))
|
||||||
|
except OSError as e:
|
||||||
|
results.append((p, f"error: {e}"))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Per-subfolder README.md ensuring + refresh
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_subfolder_readmes(scan_root: FolderScan, *, write: bool) -> list[str]:
|
||||||
|
"""For each top-level subfolder, ensure README.md exists with AUTO block."""
|
||||||
|
log: list[str] = []
|
||||||
|
for sub_name in sorted(scan_root.subfolders):
|
||||||
|
if is_sensitive(sub_name):
|
||||||
|
log.append(f"skip (sensitive): {sub_name}/")
|
||||||
|
continue
|
||||||
|
sub = scan_root.subfolders[sub_name]
|
||||||
|
readme = sub.path / "README.md"
|
||||||
|
block = render_subfolder_block(sub)
|
||||||
|
header = f"# {sub_name}\n\n_OurDigital — auto-indexed subfolder README._"
|
||||||
|
if not write:
|
||||||
|
log.append(f"would refresh: {readme}")
|
||||||
|
continue
|
||||||
|
changed, summary = splice_readme(readme, block, header_default=header)
|
||||||
|
log.append(summary)
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Reporting
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def text_report(target: Path, renames: list[Proposal], folder_renames: list[Proposal],
|
||||||
|
moves: list[Proposal], index_summary: str, sub_log: list[str]) -> str:
|
||||||
|
lines = []
|
||||||
|
lines.append(f"# our-gdrive-organizer report")
|
||||||
|
lines.append(f"target: {target}")
|
||||||
|
lines.append(f"scan time: {datetime.now().strftime('%F %T')}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Index")
|
||||||
|
lines.append(index_summary or "(skipped)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Subfolder READMEs")
|
||||||
|
if sub_log:
|
||||||
|
for line in sub_log:
|
||||||
|
lines.append(f"- {line}")
|
||||||
|
else:
|
||||||
|
lines.append("(skipped)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"## File rename proposals ({len(renames)})")
|
||||||
|
if renames:
|
||||||
|
for p in renames:
|
||||||
|
lines.append(f"- {p.src.relative_to(target)}")
|
||||||
|
lines.append(f" → {p.dst.relative_to(target)}")
|
||||||
|
lines.append(f" ({p.reason})")
|
||||||
|
else:
|
||||||
|
lines.append("(none)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"## Folder rename proposals ({len(folder_renames)})")
|
||||||
|
if folder_renames:
|
||||||
|
for p in folder_renames:
|
||||||
|
lines.append(f"- {p.src.relative_to(target)}/")
|
||||||
|
lines.append(f" → {p.dst.relative_to(target)}/")
|
||||||
|
lines.append(f" ({p.reason})")
|
||||||
|
else:
|
||||||
|
lines.append("(none)")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"## Move proposals ({len(moves)})")
|
||||||
|
if moves:
|
||||||
|
for p in moves:
|
||||||
|
lines.append(f"- {p.src.relative_to(target)}")
|
||||||
|
lines.append(f" → {p.dst.relative_to(target)}")
|
||||||
|
lines.append(f" ({p.reason})")
|
||||||
|
else:
|
||||||
|
lines.append("(none)")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="OurDigital Google Drive folder organizer",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog=__doc__,
|
||||||
|
)
|
||||||
|
parser.add_argument("target", nargs="?", default=os.getcwd(),
|
||||||
|
help="target directory (default: cwd)")
|
||||||
|
parser.add_argument("--apply", action="store_true",
|
||||||
|
help="actually perform renames and moves (default: dry-run)")
|
||||||
|
parser.add_argument("--scope", choices=["full", "index", "subreadmes",
|
||||||
|
"rename", "move"], default="full",
|
||||||
|
help="restrict the operation (default: full)")
|
||||||
|
parser.add_argument("--json", action="store_true",
|
||||||
|
help="emit machine-readable JSON instead of a text report")
|
||||||
|
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
target = Path(args.target).expanduser().resolve()
|
||||||
|
if not target.is_dir():
|
||||||
|
print(f"target is not a directory: {target}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
scan_root = scan(target, depth=3)
|
||||||
|
|
||||||
|
index_summary = ""
|
||||||
|
sub_log: list[str] = []
|
||||||
|
renames: list[Proposal] = []
|
||||||
|
folder_renames: list[Proposal] = []
|
||||||
|
moves: list[Proposal] = []
|
||||||
|
|
||||||
|
if args.scope in ("full", "index"):
|
||||||
|
readme = target / "README.md"
|
||||||
|
block = render_structure_block(scan_root)
|
||||||
|
header = f"# {target.name}\n\n_OurDigital — auto-indexed root README._\n\n## Snapshot\n\n- **Top-level files**: {len(scan_root.files)}\n- **Top-level subfolders**: {len(scan_root.subfolders)}"
|
||||||
|
changed, index_summary = splice_readme(readme, block, header_default=header)
|
||||||
|
|
||||||
|
if args.scope in ("full", "subreadmes"):
|
||||||
|
sub_log = refresh_subfolder_readmes(scan_root, write=True)
|
||||||
|
|
||||||
|
# Order matters when --apply: file renames first, then folder renames.
|
||||||
|
# If folders are renamed first, file paths inside them become invalid.
|
||||||
|
if args.scope in ("full", "rename"):
|
||||||
|
renames = propose_renames(scan_root)
|
||||||
|
if args.apply and renames:
|
||||||
|
results = apply_proposals(renames)
|
||||||
|
for p, status in results:
|
||||||
|
p.reason = f"{p.reason} [{status}]"
|
||||||
|
|
||||||
|
folder_renames = propose_folder_renames(scan_root)
|
||||||
|
if args.apply and folder_renames:
|
||||||
|
# Apply deepest folders first — renaming a parent invalidates the
|
||||||
|
# path of any nested rename. shutil.move on the deepest target
|
||||||
|
# leaves the parent's name intact and avoids stale paths.
|
||||||
|
ordered = sorted(folder_renames, key=lambda p: len(p.src.parts), reverse=True)
|
||||||
|
results = apply_proposals(ordered)
|
||||||
|
for p, status in results:
|
||||||
|
p.reason = f"{p.reason} [{status}]"
|
||||||
|
|
||||||
|
if args.scope in ("full", "move"):
|
||||||
|
moves = propose_moves(scan_root)
|
||||||
|
if args.apply and moves:
|
||||||
|
results = apply_proposals(moves)
|
||||||
|
for p, status in results:
|
||||||
|
p.reason = f"{p.reason} [{status}]"
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
out = {
|
||||||
|
"target": str(target),
|
||||||
|
"scope": args.scope,
|
||||||
|
"applied": args.apply,
|
||||||
|
"index": index_summary,
|
||||||
|
"subfolder_readmes": sub_log,
|
||||||
|
"renames": [p.as_dict() for p in renames],
|
||||||
|
"folder_renames": [p.as_dict() for p in folder_renames],
|
||||||
|
"moves": [p.as_dict() for p in moves],
|
||||||
|
}
|
||||||
|
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print(text_report(target, renames, folder_renames, moves, index_summary, sub_log))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
103
custom-skills/82-our-gdrive-organizer/desktop/SKILL.md
Normal file
103
custom-skills/82-our-gdrive-organizer/desktop/SKILL.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
name: our-gdrive-organizer
|
||||||
|
description: |
|
||||||
|
Organize a Google Drive folder under OurDigital conventions: refresh the
|
||||||
|
root README.md index, refresh per-subfolder README.md meta files, propose
|
||||||
|
filename normalizations (D.intelligence → OurDigital, client name → OOO
|
||||||
|
placeholder), and propose moves for cluttered files (screenshots, temp
|
||||||
|
downloads).
|
||||||
|
|
||||||
|
Triggers:
|
||||||
|
- "organize the Drive folder", "organize this folder"
|
||||||
|
- "refresh the index", "rescan the folder", "update README"
|
||||||
|
- "clean up cluttered files", "propose renames"
|
||||||
|
- "/organize", "/our-gdrive-organizer"
|
||||||
|
|
||||||
|
Designed for any 2nd-level Drive folder (not specific to one project).
|
||||||
|
In Claude Desktop, this is a guide — the actual filesystem operations
|
||||||
|
are performed by the user via the bundled `organizer.py` CLI in a terminal,
|
||||||
|
or by Claude with a filesystem MCP tool if available.
|
||||||
|
version: "1.0"
|
||||||
|
author: OurDigital
|
||||||
|
environment: Desktop
|
||||||
|
---
|
||||||
|
|
||||||
|
# our-gdrive-organizer (Desktop)
|
||||||
|
|
||||||
|
Guide for organizing a Drive folder under OurDigital naming + structure
|
||||||
|
conventions. In Claude Desktop without filesystem write access, walk the user
|
||||||
|
through running the bundled CLI script themselves.
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
|
||||||
|
User says any of:
|
||||||
|
- "organize my Drive folder"
|
||||||
|
- "refresh the index README"
|
||||||
|
- "scan for changes and update the README"
|
||||||
|
- "rename files to follow OurDigital convention"
|
||||||
|
- "/organize" (slash command)
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1 — Identify the target folder
|
||||||
|
|
||||||
|
Ask the user which folder they want to organize, or assume the conversation's
|
||||||
|
current 2nd-level Drive folder (e.g., `02_SEO in Action/`,
|
||||||
|
`01_Brand in Action/`, `00_OurDigital/`).
|
||||||
|
|
||||||
|
### Step 2 — Have the user run the CLI
|
||||||
|
|
||||||
|
The Python script lives in the user's local `~/Project/our-claude-skills/`
|
||||||
|
repo and is symlinked to `~/.local/bin/our-gdrive-organize`. The user runs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
our-gdrive-organize "/Users/ourdigital/Library/CloudStorage/GoogleDrive-andrew.yim@ourdigital.org/My Drive/NN_FolderName"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a dry-run by default. It always writes the README index (idempotent),
|
||||||
|
and it prints proposals for renames and moves without applying them.
|
||||||
|
|
||||||
|
Ask the user to paste back the report.
|
||||||
|
|
||||||
|
### Step 3 — Summarize and confirm
|
||||||
|
|
||||||
|
Read the report, summarize each proposed rename and move in plain language, and
|
||||||
|
ask the user which (if any) they want to apply. They can apply all:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
our-gdrive-organize "/path/to/folder" --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Or just one scope at a time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
our-gdrive-organize "/path/to/folder" --scope rename --apply
|
||||||
|
out-gdrive-organize "/path/to/folder" --scope move --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important guardrails
|
||||||
|
|
||||||
|
The script automatically skips invasive changes (renames, moves) inside:
|
||||||
|
- `04_Case Studies/`
|
||||||
|
- `99_Project Archive/`
|
||||||
|
- Any folder ending in `Archive`
|
||||||
|
- Any folder starting with `진단`
|
||||||
|
|
||||||
|
These are client-engagement records that must keep their original filenames.
|
||||||
|
If the user wants to override that, they need to edit
|
||||||
|
`SENSITIVE_SUBFOLDER_PATTERNS` in `~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py`.
|
||||||
|
|
||||||
|
## Naming convention
|
||||||
|
|
||||||
|
See `../shared/conventions.md` for the canonical spec. Highlights:
|
||||||
|
- `D.intelligence` → `OurDigital` (rebrand)
|
||||||
|
- Client-specific quote/template files → `OurDigital-{topic}-OOO {date}.{ext}`
|
||||||
|
- Top-level subfolders → `NN_descriptive name`
|
||||||
|
- Client subfolders inside Active Workspaces / Project Archive → `NN_{client name}`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"command not found: our-gdrive-organize"** — the symlink may not be set up.
|
||||||
|
Ask the user to run: `ln -s ~/Project/our-claude-skills/custom-skills/our-gdrive-organizer/code/organizer.py ~/.local/bin/our-gdrive-organize`
|
||||||
|
- **Script changes README but the user doesn't see the update** — Google Drive
|
||||||
|
sync delay; usually 5-30 seconds.
|
||||||
80
custom-skills/82-our-gdrive-organizer/docs/CHANGELOG.md
Normal file
80
custom-skills/82-our-gdrive-organizer/docs/CHANGELOG.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.2 — 2026-05-10
|
||||||
|
|
||||||
|
First content-based reorganization session. Pattern library grew from 7 to
|
||||||
|
12 gotchas. RENAME_RULES extended.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `RENAME_RULES` now catches `D intelligence` (no-dot variant) in addition
|
||||||
|
to the dot-form and `D.intelligence Lab-` prefix.
|
||||||
|
- Five new gotcha entries in `shared/patterns/gotchas.md`:
|
||||||
|
- "D.intelligence vs OurDigital" — parent company / SEO child brand;
|
||||||
|
rebrand only applies in SEO context
|
||||||
|
- "Brand-variant typos that escape the regex" — how to add new variants
|
||||||
|
without false positives
|
||||||
|
- "Real client names in 예시 자료 모음/" — half-done neutralization;
|
||||||
|
rename + TODO for cell content
|
||||||
|
- "Near-duplicate templates across 문서 양식 and 05_Working Template" —
|
||||||
|
different Doc IDs ≠ different content; needs human comparison
|
||||||
|
- "Stray screenshot that turns out to be a process diagram" — view
|
||||||
|
binary screenshots before applying default move rule
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `shared/conventions.md` "Brand prefix" section rewritten to clarify the
|
||||||
|
parent-company / child-brand distinction and the SEO-context caveat.
|
||||||
|
|
||||||
|
## 1.1 — 2026-05-10
|
||||||
|
|
||||||
|
Added folder-rename support and the patterns gotcha library.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `propose_folder_renames()` in `organizer.py` — applies `RENAME_RULES` to
|
||||||
|
folder names at depth 2+. Top-level subfolders and sensitive folders are
|
||||||
|
guarded.
|
||||||
|
- File renames execute before folder renames during `--apply` so paths stay
|
||||||
|
valid.
|
||||||
|
- New report section "Folder rename proposals" in both text and JSON output.
|
||||||
|
- `shared/patterns/` gotcha library:
|
||||||
|
- `canonical-files.md` — examples of well-named files in their proper homes
|
||||||
|
- `canonical-folders.md` — well-organized folder shapes
|
||||||
|
- `categorization-rules.md` — IF→THEN placement rules
|
||||||
|
- `gotchas.md` — known edge cases with resolutions
|
||||||
|
- Content-based reorganization workflow in `code/SKILL.md`. Claude reads
|
||||||
|
files (not just names), consults the patterns library, and proposes moves
|
||||||
|
one folder at a time for user confirmation. Applied via `Bash mv`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `text_report()` and `--json` output now include `folder_renames`.
|
||||||
|
- `code/SKILL.md` expanded with content-based workflow + patterns references.
|
||||||
|
- `shared/conventions.md` cross-references the patterns library.
|
||||||
|
|
||||||
|
## 1.0 — 2026-05-10
|
||||||
|
|
||||||
|
Initial release. Generalizes the `seo-drive-index` skill pattern to work on
|
||||||
|
any 2nd-level Drive subfolder.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Pure-Python CLI (`code/organizer.py`, stdlib only)
|
||||||
|
- Root README.md index refresh (Snapshot counts + Structure section between
|
||||||
|
AUTO-STRUCTURE markers; preserves manual Topics / Notes)
|
||||||
|
- Per-subfolder README.md meta-file ensuring (`--scope subreadmes`)
|
||||||
|
- Rename proposals enforcing `D.intelligence` → `OurDigital` rebrand
|
||||||
|
- Move proposals for screenshots and temp downloads cluttering folder roots
|
||||||
|
- Sensitive-folder skip list (`04_Case Studies`, `99_Project Archive`,
|
||||||
|
`*Archive*`, `진단*`)
|
||||||
|
- Dual SKILL.md (Claude Code + Claude Desktop)
|
||||||
|
- Slash-command alias `/organize`
|
||||||
|
- `shared/conventions.md` canonical naming spec
|
||||||
|
|
||||||
|
### Removed (during this work)
|
||||||
|
|
||||||
|
- `~/.claude/skills/seo-drive-index/` (superseded by this skill)
|
||||||
|
- `~/Library/LaunchAgents/org.ourdigital.seo-in-action-index.plist` (no
|
||||||
|
scheduled job; skill is invoked on demand instead)
|
||||||
|
- `~/.local/bin/seo-in-action-index.sh` (old bash version)
|
||||||
153
custom-skills/82-our-gdrive-organizer/shared/conventions.md
Normal file
153
custom-skills/82-our-gdrive-organizer/shared/conventions.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# OurDigital Drive Folder Conventions
|
||||||
|
|
||||||
|
Canonical reference for naming and organization patterns enforced by
|
||||||
|
`our-gdrive-organizer`. The script's rules live in `code/organizer.py` —
|
||||||
|
keep this document and that code in sync.
|
||||||
|
|
||||||
|
For the **gotcha library** (canonical examples + edge cases consulted
|
||||||
|
during interactive content-based reorganization), see `patterns/`:
|
||||||
|
- `patterns/canonical-files.md`
|
||||||
|
- `patterns/canonical-folders.md`
|
||||||
|
- `patterns/categorization-rules.md`
|
||||||
|
- `patterns/gotchas.md`
|
||||||
|
|
||||||
|
The brand prefix rules (`D.intelligence …` → `OurDigital …`) now apply to
|
||||||
|
folder names too, at depth 2+. Top-level folders (e.g., `00_Brand Management/`)
|
||||||
|
are never auto-renamed.
|
||||||
|
|
||||||
|
## Folder naming
|
||||||
|
|
||||||
|
### 2nd-level Drive folders (top-level practice areas)
|
||||||
|
|
||||||
|
`NN_descriptive name`, where NN is a 2-digit numeric prefix indicating the
|
||||||
|
"sort bucket":
|
||||||
|
|
||||||
|
| Range | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| 00–09 | Brand / core / cross-cutting |
|
||||||
|
| 10–19 | Product or specialty practice |
|
||||||
|
| 20–29 | Data / integrations |
|
||||||
|
| 90–99 | Archive |
|
||||||
|
|
||||||
|
Examples observed: `00_OurDigital`, `01_Brand in Action`, `02_SEO in Action`,
|
||||||
|
`10_OurSEO Agent`, `20_DataForSEO`, `99_Project Archive`.
|
||||||
|
|
||||||
|
### 3rd-level subfolders
|
||||||
|
|
||||||
|
`NN_descriptive name` again, scoped to the parent. Maturity ordering:
|
||||||
|
|
||||||
|
| Range | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| 00–05 | Frameworks / always-on assets |
|
||||||
|
| 10–13 | Productized tooling / specialty modules |
|
||||||
|
| 20 | Data |
|
||||||
|
| 90+ | Archive within this practice |
|
||||||
|
|
||||||
|
### Client / project subfolders
|
||||||
|
|
||||||
|
Inside `01_Active Workspaces/` or `99_Project Archive/`:
|
||||||
|
|
||||||
|
`NN_{client name}` — e.g., `00_Jamie Clinic`, `04_오현이혼상속센터`,
|
||||||
|
`05_1가다`. NN is assigned in chronological / engagement order.
|
||||||
|
|
||||||
|
### Reserved / system subfolders
|
||||||
|
|
||||||
|
- `screenshots/` — auto-created by the organizer when moving stray screenshot
|
||||||
|
files out of a parent folder root.
|
||||||
|
- `_unsorted/` — auto-created by the organizer for incomplete downloads
|
||||||
|
(`.crdownload`, `.tmp`, `.partial`).
|
||||||
|
|
||||||
|
## File naming
|
||||||
|
|
||||||
|
### Templates and quotes (in `05_Working Template/`)
|
||||||
|
|
||||||
|
`OurDigital-{Service}-OOO {date}.{ext}` or
|
||||||
|
`OurDigital-{Service}-OOOOOO {modifier}.{ext}`.
|
||||||
|
|
||||||
|
`OOO` and `OOOOOO` are the canonical placeholder strings for "client name
|
||||||
|
goes here" — chosen to be visually obvious and easy to grep for. Use
|
||||||
|
whichever makes more visual sense in the filename.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
|
||||||
|
- `OurDigital-SEO Treatment-OOO 호텔 견적-20240903.gsheet`
|
||||||
|
- `OurDigital-OOO 호텔 SEO Basic 패키지-20250416.gsheet`
|
||||||
|
- `OurDigital-Data Access & Management Agreement - 2026.gsheet`
|
||||||
|
|
||||||
|
### Brand prefix — parent company / SEO child brand
|
||||||
|
|
||||||
|
D.intelligence is the **parent company**; OurDigital is its **SEO-specialty
|
||||||
|
child brand**. The rebrand-to-OurDigital rule only applies when the asset
|
||||||
|
is SEO-related.
|
||||||
|
|
||||||
|
Rules (filesystem rename only — does not touch cell/body content):
|
||||||
|
|
||||||
|
- `D.intelligence Lab-` → `OurDigital-`
|
||||||
|
- `D.intelligence` (anywhere) → `OurDigital`
|
||||||
|
- `D intelligence` (no-dot variant) → `OurDigital`
|
||||||
|
|
||||||
|
The organizer applies these to both files and folders at depth 2+. Top-level
|
||||||
|
folders (`NN_…`) are never auto-renamed.
|
||||||
|
|
||||||
|
**Important**: The script doesn't enforce the SEO-context caveat. When you
|
||||||
|
point the organizer at the whole Drive Stream or a non-SEO folder, it will
|
||||||
|
rename ALL D.intelligence references — which is wrong outside SEO context.
|
||||||
|
Only run the organizer on SEO folders (`02_SEO in Action/`,
|
||||||
|
`00_OurDigital/04_SEO/`, etc.). For non-SEO D.intelligence assets, do not
|
||||||
|
rename — they belong to the parent company.
|
||||||
|
|
||||||
|
See `patterns/gotchas.md` "D.intelligence vs OurDigital" for full guidance.
|
||||||
|
|
||||||
|
### Archive client files
|
||||||
|
|
||||||
|
Files inside `99_Project Archive/{client}/` should KEEP their original
|
||||||
|
filenames including real client names. The script skips these folders for
|
||||||
|
rename/move operations.
|
||||||
|
|
||||||
|
### Date stamps
|
||||||
|
|
||||||
|
Use `YYYYMMDD` (no separator) when embedding a date in a filename:
|
||||||
|
`OurDigital-Data Access & Management Agreement - 2026.gsheet`
|
||||||
|
`OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
|
||||||
|
|
||||||
|
When a date range is needed: `YYYYMMDD-YYYYMMDD`.
|
||||||
|
|
||||||
|
### Extensions and stub files
|
||||||
|
|
||||||
|
Google Drive Apps formats appear as 180-byte JSON stubs on the local
|
||||||
|
filesystem with these extensions:
|
||||||
|
|
||||||
|
| Drive type | Extension |
|
||||||
|
|-----------|-----------|
|
||||||
|
| Docs | `.gdoc` |
|
||||||
|
| Sheets | `.gsheet` |
|
||||||
|
| Slides | `.gslides`|
|
||||||
|
| Forms | `.gform` |
|
||||||
|
|
||||||
|
Renaming the stub on the filesystem only changes the local filename — the
|
||||||
|
backing Google document, its Doc ID, sharing links, and revision history are
|
||||||
|
untouched. Safe to rename. Cell-level / body-level changes still require
|
||||||
|
opening the file in the corresponding Google app.
|
||||||
|
|
||||||
|
## What the organizer does NOT enforce
|
||||||
|
|
||||||
|
- Cell content inside `.gsheet`, body content inside `.gdoc`, slide content
|
||||||
|
inside `.gslides`, or any binary file (`.pdf`, `.pptx`, `.xlsx`). Those need
|
||||||
|
manual editing in the source app. Track them with a `TODO.md` if needed
|
||||||
|
(see `02_SEO in Action/98_Training/TODO.md` for an example pattern).
|
||||||
|
- Folder maturity ordering — the NN prefix system is a convention, but the
|
||||||
|
script doesn't reorder or renumber folders.
|
||||||
|
- Per-folder `README.md` content beyond the AUTO-STRUCTURE block — manual
|
||||||
|
Topics / Notes sections are preserved.
|
||||||
|
|
||||||
|
## Sensitive folders (never auto-modified)
|
||||||
|
|
||||||
|
Internal regex list (`SENSITIVE_SUBFOLDER_PATTERNS` in `organizer.py`):
|
||||||
|
|
||||||
|
- `^04_Case Studies$`
|
||||||
|
- `^99_Project Archive$`
|
||||||
|
- `.*Archive$` (case-insensitive)
|
||||||
|
- `^진단.*$`
|
||||||
|
|
||||||
|
These folders are still indexed (counts appear in the README structure),
|
||||||
|
but no files inside them are renamed or moved.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Patterns library — gotcha folder
|
||||||
|
|
||||||
|
The "gotcha folder" Claude consults when doing **content-based reorganization**.
|
||||||
|
Filename pattern matching is handled deterministically by `code/organizer.py`;
|
||||||
|
this folder is for the judgment-calls Claude makes when reading file contents
|
||||||
|
to decide where something belongs.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `canonical-files.md` — examples of well-named files in their proper homes.
|
||||||
|
When Claude sees a file and isn't sure where it goes, it pattern-matches
|
||||||
|
against these canonical examples.
|
||||||
|
- `canonical-folders.md` — well-organized folder shapes. What goes in
|
||||||
|
`02_SEO Audit Toolkit/` vs `03_SEO Hack Library/` vs `99_Project Archive/`.
|
||||||
|
- `categorization-rules.md` — explicit IF→THEN rules for placement decisions.
|
||||||
|
These trump heuristics; if a rule applies, follow it.
|
||||||
|
- `gotchas.md` — edge cases, ambiguous files, common mistakes, and how to
|
||||||
|
resolve them.
|
||||||
|
|
||||||
|
## How to extend
|
||||||
|
|
||||||
|
When the user encounters a new edge case or pattern that the script can't
|
||||||
|
handle automatically, add it here:
|
||||||
|
1. If the rule is deterministic (filename pattern → action), put it in
|
||||||
|
`code/organizer.py` (`RENAME_RULES`, `MOVE_RULES`) and document in
|
||||||
|
`shared/conventions.md`.
|
||||||
|
2. If the rule is judgment-based (read content, decide), add a new entry
|
||||||
|
here and reference it from `code/SKILL.md`'s content-based workflow.
|
||||||
|
|
||||||
|
The patterns library should grow over time as Claude + user encounter more
|
||||||
|
edge cases. Each addition is a "gotcha" the system has learned.
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Canonical file examples
|
||||||
|
|
||||||
|
Reference set of well-named files in their correct homes. When Claude sees
|
||||||
|
an unfamiliar file, it should pattern-match against these and ask: "what
|
||||||
|
home best fits this file's shape?"
|
||||||
|
|
||||||
|
## Templates / blank artifacts → `05_Working Template/`
|
||||||
|
|
||||||
|
Filenames are generic and use placeholder client names (`OOO`, `OOOOOO`):
|
||||||
|
|
||||||
|
- `OurDigital-SEO Audit Template-20240922.gsheet`
|
||||||
|
- `OurDigital-Local SEO Audit 견적서.xlsx`
|
||||||
|
- `OurDigital-OOO 호텔 SEO Basic 패키지-20250416.gsheet`
|
||||||
|
- `OurDigital-OOOOOO SEO Audit & Treatment_20250120.gsheet`
|
||||||
|
- `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
|
||||||
|
- `OurDigital-on-Page SEO-T&D Generator.gsheet`
|
||||||
|
- `OurDigital-SEO job tracker-2025.gsheet`
|
||||||
|
- `OurDigital-Data Access & Management Agreement - 2026.gsheet`
|
||||||
|
|
||||||
|
Pattern: `OurDigital-{topic}-{OOO/OOOOOO placeholder}-{YYYYMMDD}.{ext}`
|
||||||
|
|
||||||
|
## Audit toolkit reusable assets → `02_SEO Audit Toolkit/`
|
||||||
|
|
||||||
|
Reusable proposal templates, on-page elements kits, framework documents:
|
||||||
|
|
||||||
|
- `D.intelligence Lab-SEO Audit & Treatment 2025-Proposal-Template.pdf`
|
||||||
|
(legacy — should be renamed to `OurDigital-…`)
|
||||||
|
- `OurDigital-on-Pages Elements Kit-20250529.gsheet`
|
||||||
|
- `SEO Audit & Check_NotebookLM.wav`
|
||||||
|
|
||||||
|
Subfolders typical here: `참고 자료/`, `예시 자료 모음/`, `문서 양식/`,
|
||||||
|
`교육 자료/`, `Google Business Profile/`, `robots.txt/`, `sitemap.xml/`.
|
||||||
|
|
||||||
|
## Reference library / external research → `03_SEO Hack Library/`
|
||||||
|
|
||||||
|
Third-party PDFs, webinar materials, structured-data references, academic
|
||||||
|
papers, framework PPTs:
|
||||||
|
|
||||||
|
- `1-1-what-is-structured-data-structured-data-for-beginners.pdf`
|
||||||
|
- `2-1-the-yoast-seo-graph-structured-data-for-beginners.pdf`
|
||||||
|
- `SEO_Advanced_구조화 된 데이터 스키마의 이해.pptx`
|
||||||
|
- `OurDigital-SEO-curl-commands.pdf`
|
||||||
|
- `opensurvey_trend_search_2024.pdf`
|
||||||
|
|
||||||
|
Pattern: external-source-derived names (paper IDs, course module numbering,
|
||||||
|
vendor names) — not OurDigital-prefixed.
|
||||||
|
|
||||||
|
## Active engagement working files → `01_Active Workspaces/{NN_client}/`
|
||||||
|
|
||||||
|
Real client name in path; files reference the engagement directly:
|
||||||
|
|
||||||
|
- `01_Active Workspaces/00_Jamie Clinic/JAM-FAQ 엔트리 추출-정리-20260304.gsheet`
|
||||||
|
- `01_Active Workspaces/01_신라호텔 SEO/SLA - SEO KPIs and goal setup_20251020.gsheet`
|
||||||
|
- `01_Active Workspaces/01_신라호텔 SEO/Shilla_SEO_Performance_Scorecard.xlsx`
|
||||||
|
- `01_Active Workspaces/02_K Beauty CC 우승기업/[Note] K-Beauty CC 우승 기업 컨설팅_20260210.gdoc`
|
||||||
|
|
||||||
|
Pattern: `{ClientPrefix}-{topic}-{date}.{ext}` or `[Note] {description}.gdoc`.
|
||||||
|
Client name appears in BOTH the folder name AND the filename — that's expected.
|
||||||
|
|
||||||
|
## Past engagement deliverables → `04_Case Studies/`
|
||||||
|
|
||||||
|
Polished proposal/audit PDFs from completed work, used as portfolio reference.
|
||||||
|
Real client names retained:
|
||||||
|
|
||||||
|
- `신라호텔 SEO 제안서 개요.pdf`
|
||||||
|
- `소노 인터내셔널 Preliminary SEO Audit.pdf`
|
||||||
|
- `JHR-조선호텔 웹사이트 리뉴얼 SEO 전략 검토-20260303.gdoc`
|
||||||
|
- `그랜드 머큐어 임페리얼 팰리스 호텔 SEO 진단 컨설팅 계약서.pdf`
|
||||||
|
|
||||||
|
Pattern: client name first, deliverable type, optional date. Always real
|
||||||
|
names (this folder is the case-study portfolio).
|
||||||
|
|
||||||
|
## Archived completed projects → `99_Project Archive/{NN_client}/`
|
||||||
|
|
||||||
|
Full engagement archive — all working files preserved with original names:
|
||||||
|
|
||||||
|
- `99_Project Archive/01_그랜드 머큐어 임페리얼 SEO Basic/00_성과 측정 기준/...`
|
||||||
|
- `99_Project Archive/02_TNS 유학원 SEO 진단/Crawling data/...`
|
||||||
|
- `99_Project Archive/04_오현이혼상속센터/D.intelligence Lab-SEO Coaching-양제민 변호사-20240806.gsheet`
|
||||||
|
|
||||||
|
Pattern: client name in folder, original delivery filenames preserved
|
||||||
|
(do NOT normalize — these are historical records).
|
||||||
|
|
||||||
|
## Training materials → `98_Training/`
|
||||||
|
|
||||||
|
Generic, anonymized lesson content. Client-specific lesson notes belong
|
||||||
|
in the archive, NOT here:
|
||||||
|
|
||||||
|
- `98_Training/01_수업 계획_코스/Content Marketing Workout.md`
|
||||||
|
- `98_Training/01_수업 계획_코스/SEO 입문 기본교육 과정.gdoc`
|
||||||
|
- `98_Training/04_강의자료 스크린샷/Screenshot 2024-09-22 at *.png`
|
||||||
|
- `98_Training/09_외부 강의/Shopee e-Commerce SEO/Global e-Commerce SEO in Action 101.pdf`
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# Canonical folder shapes
|
||||||
|
|
||||||
|
What "well-organized" looks like at each level. Use these as anchors when
|
||||||
|
deciding whether to create a new subfolder or which existing one a file
|
||||||
|
should live in.
|
||||||
|
|
||||||
|
## Top-level practice area (e.g., `02_SEO in Action/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
NN_Practice Area/
|
||||||
|
├── README.md # auto-indexed root
|
||||||
|
├── 00_Brand Management/ # 00–05: frameworks / always-on
|
||||||
|
├── 01_Active Workspaces/ # current client engagements
|
||||||
|
├── 02_{Practice} Audit Toolkit/ # reusable audit assets
|
||||||
|
├── 03_{Practice} Hack Library/ # external reference / research
|
||||||
|
├── 04_Case Studies/ # past engagement portfolio
|
||||||
|
├── 05_Working Template/ # blank quote/audit/coaching templates
|
||||||
|
├── 10_{Productized Tool}/ # 10–13: productized specialty
|
||||||
|
├── 11_{Specialty Module}/
|
||||||
|
├── 20_{Data Source}/ # 20s: data integrations
|
||||||
|
├── 98_Training/ # 98: training materials
|
||||||
|
└── 99_Project Archive/ # 99: completed engagements
|
||||||
|
```
|
||||||
|
|
||||||
|
Maturity convention by NN range:
|
||||||
|
- 00–05: foundational / framework / always-on
|
||||||
|
- 10–13: productized tools / specialties
|
||||||
|
- 20–29: data integrations
|
||||||
|
- 90–99: archive / training / closed work
|
||||||
|
|
||||||
|
## Active workspace shape (`01_Active Workspaces/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
01_Active Workspaces/
|
||||||
|
├── README.md
|
||||||
|
├── 00_{Client A}/
|
||||||
|
│ ├── {client-prefix}-{deliverable}-{date}.{ext}
|
||||||
|
│ ├── [Note] {topic} {date}.gdoc
|
||||||
|
│ └── AI Reference/ # if engagement uses AI tools
|
||||||
|
└── 01_{Client B}/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each client gets one subfolder, NN-prefixed in chronological onboarding
|
||||||
|
order. Files use a short client prefix (e.g., `JAM-` for Jamie, `SLA-` for
|
||||||
|
신라호텔, `JHR-` for 조선호텔).
|
||||||
|
|
||||||
|
## Case study shape (`04_Case Studies/`)
|
||||||
|
|
||||||
|
Flat — files at the root, no subfolders:
|
||||||
|
|
||||||
|
```
|
||||||
|
04_Case Studies/
|
||||||
|
├── README.md
|
||||||
|
├── {ClientName} SEO {DeliverableType}.pdf
|
||||||
|
├── {ClientName} {DeliverableType} {date}.gdoc
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Each file is a portfolio-quality deliverable. No working files here.
|
||||||
|
|
||||||
|
## Project archive shape (`99_Project Archive/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
99_Project Archive/
|
||||||
|
├── README.md
|
||||||
|
├── 01_{First archived client}/
|
||||||
|
│ ├── 00_{topic}/ # may have its own NN-prefixed subfolders
|
||||||
|
│ ├── 09_사전 진단 / # diagnostic phase
|
||||||
|
│ ├── 10_계약 관련/ # contract phase
|
||||||
|
│ ├── 90_고객사 수급자료/ # client-supplied materials
|
||||||
|
│ └── {original-filename}.{ext} # plus loose root files
|
||||||
|
└── 02_{Second archived client}/
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Sub-folder structure inside each archived client mirrors the original
|
||||||
|
engagement folder layout — nothing renamed or normalized.
|
||||||
|
|
||||||
|
## Audit toolkit shape (`02_SEO Audit Toolkit/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
02_{Practice} Audit Toolkit/
|
||||||
|
├── README.md
|
||||||
|
├── 참고 자료/ # references
|
||||||
|
├── 예시 자료 모음/ # neutralized example deliverables
|
||||||
|
├── 문서 양식/ # blank document forms
|
||||||
|
├── 교육 자료/ # internal training assets
|
||||||
|
├── {tool}_standard check_{date}/ # versioned tool snapshots
|
||||||
|
├── Google Business Profile/ # specialty subdomain
|
||||||
|
├── robots.txt/ # specialty subdomain
|
||||||
|
└── sitemap.xml/ # specialty subdomain
|
||||||
|
```
|
||||||
|
|
||||||
|
Subfolders here are **topic-grouped reusable assets**. Each subfolder is
|
||||||
|
purpose-named (Korean OK) — not NN-prefixed because order doesn't matter.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Categorization rules
|
||||||
|
|
||||||
|
IF→THEN rules for deciding where a file belongs. Apply in order; first
|
||||||
|
match wins.
|
||||||
|
|
||||||
|
## Hard-coded rules (always honor)
|
||||||
|
|
||||||
|
| If file… | Then it belongs in… |
|
||||||
|
|---|---|
|
||||||
|
| Lives inside `04_Case Studies/` or `99_Project Archive/` | leave it (sensitive — never auto-move) |
|
||||||
|
| Filename starts with `Screenshot YYYY-MM-DD at` | parent folder's `screenshots/` subdir |
|
||||||
|
| Has extension `.crdownload`, `.tmp`, `.partial` | `_unsorted/` (temp / interrupted) |
|
||||||
|
| Has `D.intelligence` in name | rename to `OurDigital` (filesystem rename only) |
|
||||||
|
|
||||||
|
## Content + filename heuristics (judgment calls)
|
||||||
|
|
||||||
|
### Templates vs Case Studies
|
||||||
|
|
||||||
|
| Signal | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| Contains placeholder strings `OOO` / `OOOOOO` in filename | Template — `05_Working Template/` |
|
||||||
|
| Filename is `OurDigital-{topic}-{specific-client}-{date}` AND client is real | Case Study or Active Workspace, NOT template |
|
||||||
|
| Real client name in filename + signed contract content | `04_Case Studies/` |
|
||||||
|
| Real client name + WIP / current-quarter date | `01_Active Workspaces/{NN_client}/` |
|
||||||
|
| Real client name + closure-marker content (final report, sign-off) | `99_Project Archive/{NN_client}/` |
|
||||||
|
|
||||||
|
### Reference library vs Audit toolkit
|
||||||
|
|
||||||
|
| Signal | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| Source is external (academic paper, vendor whitepaper, foreign URL slug, paper ID `2286-Article…`) | `03_{Practice} Hack Library/` |
|
||||||
|
| Source is OurDigital-authored, intended as reusable proposal/audit asset | `02_{Practice} Audit Toolkit/` |
|
||||||
|
| Korean filename about a niche specialty (sitemap.xml, robots.txt) | `02_…/{specialty}/` subdirectory |
|
||||||
|
|
||||||
|
### Training material vs Engagement notes
|
||||||
|
|
||||||
|
| Signal | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| Generic title, no client name (`SEO 입문 기본교육 과정.pdf`) | `98_Training/01_수업 계획_코스/` |
|
||||||
|
| Lesson notes referencing real lawyer/doctor/consultant by name | `99_Project Archive/{NN_client}/` (it's a record of the engagement) |
|
||||||
|
| Screenshot from a lesson | `98_Training/04_강의자료 스크린샷/` if generic; archive if it shows specific client data |
|
||||||
|
| External lecture deck (Shopee, Google event) | `98_Training/09_외부 강의/{Topic}/` |
|
||||||
|
|
||||||
|
### Quote / proposal / contract documents
|
||||||
|
|
||||||
|
| Signal | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| Quote sheet for a specific client, engagement won → archived | `99_Project Archive/{NN_client}/` |
|
||||||
|
| Quote sheet for a specific client, engagement won → ongoing | `01_Active Workspaces/{NN_client}/` |
|
||||||
|
| Quote sheet that's been neutralized (client → OOO) | `05_Working Template/` |
|
||||||
|
| Signed contract PDF | usually `04_Case Studies/` (portfolio) or archive |
|
||||||
|
|
||||||
|
## Anti-rules (do NOT do these)
|
||||||
|
|
||||||
|
- **Do not rename files inside `04_Case Studies/` or `99_Project Archive/`**
|
||||||
|
even if they have `D.intelligence` in the name. Those are historical records
|
||||||
|
and the brand at the time was D.intelligence — preserving that is correct.
|
||||||
|
- **Do not change Korean folder names to English** (or vice versa). Language
|
||||||
|
is intentional.
|
||||||
|
- **Do not collapse client subfolders** even if they only have 1–2 files.
|
||||||
|
- **Do not move things to satisfy "everything balanced"** — empty subfolders
|
||||||
|
are fine if they represent reserved slots.
|
||||||
275
custom-skills/82-our-gdrive-organizer/shared/patterns/gotchas.md
Normal file
275
custom-skills/82-our-gdrive-organizer/shared/patterns/gotchas.md
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
# Gotchas
|
||||||
|
|
||||||
|
Edge cases the system has learned. When in doubt during interactive
|
||||||
|
content-based reorganization, check here first.
|
||||||
|
|
||||||
|
Each gotcha follows the format: **Pattern → Why it's tricky → Resolution**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Lesson notes for a specific lawyer/doctor/consultant
|
||||||
|
|
||||||
|
**Pattern**: `Notes – [레슨] SEO 진단 & 관리 수업 - 양제민 변호사 6회차 (대면).gdoc`
|
||||||
|
or similar lesson-format file referencing a real client by name.
|
||||||
|
|
||||||
|
**Why tricky**: Looks like training material (it IS a lesson note), but it's
|
||||||
|
client-specific (양제민 variant of 오현이혼상속센터 engagement).
|
||||||
|
|
||||||
|
**Resolution**: Belongs in `99_Project Archive/{NN_그_클라이언트}/`, NOT in
|
||||||
|
`98_Training/`. Training is for generic, reusable content. Client-specific
|
||||||
|
lesson notes are engagement records.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Quote sheet with `D.intelligence Lab-` prefix and a client name
|
||||||
|
|
||||||
|
**Pattern**: `D.intelligence Lab-SEO Coaching-오현법률사무소-20240612.gsheet`
|
||||||
|
|
||||||
|
**Why tricky**: Has both a brand-rebrand candidate (`D.intelligence Lab-` →
|
||||||
|
`OurDigital-`) AND a real client name that should become `OOO`. But cell
|
||||||
|
content can't be normalized by filesystem rename.
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
1. Filesystem rename: `OurDigital-SEO Coaching-OOO 견적-20240612.gsheet`
|
||||||
|
2. Add to a `TODO.md` reminding the user to open the sheet in Google Sheets
|
||||||
|
and replace `오현법률사무소` and contact info inside cells with `OOO` /
|
||||||
|
placeholder text.
|
||||||
|
3. Move to `05_Working Template/` once both filesystem AND cell content are
|
||||||
|
neutralized. Until then, leaving the file in place with the rename done
|
||||||
|
is a valid intermediate state.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Files at the root that look like they should be in a subfolder
|
||||||
|
|
||||||
|
**Pattern**: A top-level subfolder root contains 30+ loose files plus 0
|
||||||
|
subfolders. E.g., a `screenshots` collection directly at the top of
|
||||||
|
`98_Training/`.
|
||||||
|
|
||||||
|
**Why tricky**: The script's `MOVE_RULES` only catches very specific
|
||||||
|
patterns (`Screenshot YYYY-MM-DD…`). Manual moves often need judgment —
|
||||||
|
which subfolder should be created, what should be its name?
|
||||||
|
|
||||||
|
**Resolution**: Interactive content-based mode. Claude reads filenames in
|
||||||
|
batches, proposes a subfolder name (matching local language convention —
|
||||||
|
Korean if rest of folder is Korean), confirms with user, then moves with
|
||||||
|
`mv`. Update the parent README afterward via `our-gdrive-organize --scope index`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Korean vs English filename mixing inside one subfolder
|
||||||
|
|
||||||
|
**Pattern**: `02_SEO Audit Toolkit/` contains both English files
|
||||||
|
(`OurDigital-on-Pages Elements Kit-20250529.gsheet`) and Korean subfolders
|
||||||
|
(`참고 자료/`, `문서 양식/`).
|
||||||
|
|
||||||
|
**Why tricky**: Looks inconsistent at first glance, but is intentional —
|
||||||
|
files use English when they're "OurDigital products" and Korean when
|
||||||
|
they're "Korean-language reference materials."
|
||||||
|
|
||||||
|
**Resolution**: Don't normalize. Language tracks function:
|
||||||
|
- OurDigital-authored asset → English filename, OurDigital prefix
|
||||||
|
- External / Korean reference → Korean filename
|
||||||
|
- Subfolder for grouping Korean references → Korean folder name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Empty subfolders
|
||||||
|
|
||||||
|
**Pattern**: `99_Project Archive/03_소노펠리체CC Local SEO/09_수급 정보/`
|
||||||
|
contains 0 files.
|
||||||
|
|
||||||
|
**Why tricky**: Tempting to delete to "clean up." But empty subfolders often
|
||||||
|
represent reserved engagement phases that the project just didn't reach,
|
||||||
|
or pending document deliveries.
|
||||||
|
|
||||||
|
**Resolution**: Leave empty subfolders alone unless the user explicitly
|
||||||
|
says to clean them up. They don't break anything.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Numbered duplicates: `(1)`, `(2)` suffixes
|
||||||
|
|
||||||
|
**Pattern**: `D.intelligence Lab-SEO Coaching-오현법률사무소-20240612 (1).gsheet`
|
||||||
|
exists alongside the same name without the `(1)`.
|
||||||
|
|
||||||
|
**Why tricky**: Looks like a Drive sync duplicate, but `cmp` shows different
|
||||||
|
bytes — they're DIFFERENT Drive documents that happen to have the same name.
|
||||||
|
|
||||||
|
**Resolution**: `cmp` the two `.gsheet` stubs. If different (which they
|
||||||
|
usually are for `(N)`-suffixed files), preserve both with a suffix like
|
||||||
|
`(v1)`, `(v2)`, or `(legacy)`. Never overwrite blindly. If actually
|
||||||
|
identical bytes, ask user which to keep.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `_unsorted/` accumulates over time
|
||||||
|
|
||||||
|
**Pattern**: After several `--scope move --apply` runs, `_unsorted/`
|
||||||
|
accumulates `.crdownload` / `.tmp` files that the user never went back to.
|
||||||
|
|
||||||
|
**Why tricky**: These are usually legitimate trash but occasionally a real
|
||||||
|
in-progress download.
|
||||||
|
|
||||||
|
**Resolution**: Don't auto-delete. Periodically prompt the user: "Your
|
||||||
|
`_unsorted/` has N files older than 30 days. Want to review?"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### A folder that mixes archive + active work
|
||||||
|
|
||||||
|
**Pattern**: A subfolder under `01_Active Workspaces/` contains both
|
||||||
|
ongoing work AND files from a finished engagement that should have been
|
||||||
|
archived.
|
||||||
|
|
||||||
|
**Why tricky**: Hard to tell from filenames alone. Need to check mtimes
|
||||||
|
and content (last-modified-recently → active; older + closure-marker docs →
|
||||||
|
should be archived).
|
||||||
|
|
||||||
|
**Resolution**: Interactive mode. Claude reads file mtimes + samples
|
||||||
|
content, proposes splitting into a new `99_Project Archive/{NN_client}/`
|
||||||
|
entry. User confirms before moving.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D.intelligence vs OurDigital — parent company / child brand
|
||||||
|
|
||||||
|
**Pattern**: Files or folders named `D.intelligence …`, `D intelligence …`,
|
||||||
|
or with the legacy `D.intelligence Lab-` prefix.
|
||||||
|
|
||||||
|
**Why tricky**: D.intelligence is the **parent company**; OurDigital is its
|
||||||
|
**SEO-specialty child brand**. The rebrand-to-OurDigital rule only applies
|
||||||
|
when the asset is SEO-related. Non-SEO D.intelligence assets (consulting,
|
||||||
|
data, training in other practices) keep the D.intelligence name because
|
||||||
|
they belong to the parent company, not to OurDigital.
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
- Inside an SEO context (`02_SEO in Action/`, `00_OurDigital/04_SEO/`, or
|
||||||
|
any folder whose name contains "SEO"): apply the standard rename
|
||||||
|
`D.intelligence … → OurDigital …`.
|
||||||
|
- Outside SEO context: **do not rename**. Flag for user review and
|
||||||
|
document the asset's intended owning practice.
|
||||||
|
- The `RENAME_RULES` in `code/organizer.py` cover three variants
|
||||||
|
(`D.intelligence Lab-`, `D.intelligence`, and the no-dot `D intelligence`
|
||||||
|
typo). The rules don't enforce the SEO-context caveat — that's the
|
||||||
|
caller's responsibility (point the script at an SEO folder, not the
|
||||||
|
whole Drive Stream).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Brand-variant typos that escape the regex
|
||||||
|
|
||||||
|
**Pattern**: A filename uses an off-spec spelling of `D.intelligence` —
|
||||||
|
e.g., `D intelligence SEO Audit & Treatment.pdf` (no dot), or
|
||||||
|
`OurDigitial-…` (transposed letters), or `Techincal SEO` (transposed).
|
||||||
|
|
||||||
|
**Why tricky**: The standard `D\.intelligence` regex requires the literal
|
||||||
|
dot, so the no-dot variant slips through. Same for OurDigital typos —
|
||||||
|
they don't match the brand pattern at all and look like normal filenames.
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
1. When you find one during a manual review, do the rename via `mv` and
|
||||||
|
immediately consider whether to add a regex variant to `RENAME_RULES`.
|
||||||
|
2. The current rules cover: `D.intelligence Lab-`, `D.intelligence`, and
|
||||||
|
`D intelligence` (no-dot, word-boundaries to avoid false positives).
|
||||||
|
3. Common typos that are NOT in regex (because they're one-off mistakes):
|
||||||
|
`OurDigitial`, `Techincal`. Catch with `mv` during manual review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Real client names in `예시 자료 모음/`
|
||||||
|
|
||||||
|
**Pattern**: Files in `02_…/예시 자료 모음/` that still have real client
|
||||||
|
names in the filename — e.g., `OurDigital-SEO Audit-1gada.com-20240703.xlsx`,
|
||||||
|
`OurDigital-Sono International-Preliminary SEO Audit-20240927.gdoc`.
|
||||||
|
|
||||||
|
**Why tricky**: The folder's canonical role is "neutralized example
|
||||||
|
deliverables" — examples to show in pre-sales without exposing real client
|
||||||
|
data. A file with a real client name in this folder is a half-done
|
||||||
|
neutralization. The original engagement copy usually exists elsewhere
|
||||||
|
(`99_Project Archive/{NN_client}/` or `04_Case Studies/`).
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
1. Filesystem rename to neutralize the FILENAME using `OOO`-style
|
||||||
|
placeholders: `OurDigital-OOO 호텔 체인-Preliminary SEO Audit-…gdoc`.
|
||||||
|
2. Add to a `TODO.md` reminding the user to also neutralize CELL CONTENT
|
||||||
|
(real names, contact info, URLs, keyword examples) inside the source
|
||||||
|
Sheet/Doc — filesystem rename doesn't touch cell content.
|
||||||
|
3. Don't delete the file even though the original exists elsewhere — the
|
||||||
|
neutralized example serves a different purpose (sales / training) than
|
||||||
|
the archived original (engagement record).
|
||||||
|
4. If the original doesn't exist elsewhere, copy it to the right archive
|
||||||
|
folder FIRST before neutralizing the example.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Near-duplicate templates across `문서 양식/` and `05_Working Template/`
|
||||||
|
|
||||||
|
**Pattern**: Same template name in both
|
||||||
|
`02_…/문서 양식/OurDigital-SEO Audit Template-{date1}.gsheet` and
|
||||||
|
`05_Working Template/OurDigital-SEO Audit Template-{date2}.gsheet` with
|
||||||
|
different dates (and different Doc IDs).
|
||||||
|
|
||||||
|
**Why tricky**: Looks like the same template at v1 and v2 (good cleanup
|
||||||
|
target — keep the newer, archive the older). But sometimes they're
|
||||||
|
genuinely different templates that just happen to share a name.
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
1. `cmp` the .gsheet stubs first. Always different (different Doc IDs)
|
||||||
|
for files at different dates — that just confirms they're separate
|
||||||
|
Drive Docs, not bytes-identical stubs.
|
||||||
|
2. The Doc IDs alone can't tell you whether the cell content is similar.
|
||||||
|
Open both Sheets in Google Drive. Usually one is a direct refinement
|
||||||
|
of the other (older = v1, newer = v2 with added rows/columns).
|
||||||
|
3. If clearly v1 / v2 of same template: delete v1, OR move v1 to
|
||||||
|
`05_Working Template/` with `(legacy v1)` suffix.
|
||||||
|
4. If genuinely different (e.g., one is "quick check" and other is
|
||||||
|
"comprehensive"): rename to disambiguate explicitly.
|
||||||
|
5. Always defer to the user for the open-and-compare step. Add to
|
||||||
|
`TODO.md` with both Doc IDs + paths so the user knows what to compare.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Stray screenshot that turns out to be a process diagram
|
||||||
|
|
||||||
|
**Pattern**: A `Screenshot_YYYY-MM-DD…png` in a folder of templates that
|
||||||
|
the script's `MOVE_RULES` would normally route to a `screenshots/` subdir.
|
||||||
|
|
||||||
|
**Why tricky**: The MOVE_RULES regex (`^Screenshot \d{4}-\d{2}-\d{2}…`)
|
||||||
|
treats anything with that prefix as junk to be tucked away. But sometimes
|
||||||
|
the screenshot is actually a captured workflow diagram, org chart, or
|
||||||
|
reference visualization that has real value AND a meaningful home elsewhere.
|
||||||
|
|
||||||
|
**Resolution**:
|
||||||
|
- Always view the screenshot before moving it (use `Read` on the .png).
|
||||||
|
- If it's a diagram / reference visualization: rename to a descriptive
|
||||||
|
filename and move to the most relevant subfolder (often `참고 자료/`
|
||||||
|
for audit-toolkit context, `docs/` for code-related).
|
||||||
|
- If it's an actual junk screenshot (UI snapshot during work): apply the
|
||||||
|
default rule and move to `screenshots/`.
|
||||||
|
- The MOVE_RULES regex pattern uses the macOS default
|
||||||
|
`Screenshot YYYY-MM-DD at HH.MM.SS AM/PM.png` (with spaces). The
|
||||||
|
underscore variant `Screenshot_YYYY-MM-DD_at_*` does NOT match —
|
||||||
|
catch those manually during content review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding new gotchas
|
||||||
|
|
||||||
|
When you (Claude) encounter a new ambiguous case during a content-based
|
||||||
|
reorganization session, add an entry here BEFORE moving on. Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
### Short pattern title
|
||||||
|
|
||||||
|
**Pattern**: filename / structure example.
|
||||||
|
|
||||||
|
**Why tricky**: what makes this hard.
|
||||||
|
|
||||||
|
**Resolution**: what to do.
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how the system gets smarter over time. The patterns library is
|
||||||
|
the institutional memory.
|
||||||
Reference in New Issue
Block a user