
Doc Consolidation
- 102 installs
- 325 repo stars
- Updated August 2, 2026
- athola/claude-night-market
Scan a repo for stray LLM-generated markdown reports and queue them for consolidation into permanent docs.
About
doc-consolidation gives solo builders a deterministic module for spotting markdown that should merge into real documentation instead of cluttering the repository root. It ranks detection signals: untracked .md outside blessed folders, shouty report-style filenames, and LLM-typical headings in the first hundred lines—with carve-outs for README, LICENSE, and CHANGELOG. The workflow is git-aware so agent sessions that spawn API_REVIEW_REPORT.md or MIGRATION_ANALYSIS.md get flagged early. Multi-phase value shows up whenever you tidy after a build spike, pre-launch doc pass, or post-incident write-ups. Teams using Claude night-market style agent packs can run the same rules before commit to keep skills/, commands/, and docs/ authoritative while transient analysis files get consolidated or archived.
- Three prioritized signals: git-untracked .md, ALL_CAPS report filenames, and LLM content markers
- git status porcelain recipe to list untracked markdown candidates
- Explicit exclude lists for docs/, skills/, modules/, commands/, agents/, .github/, and standard README/LICENSE names
- ALL_CAPS patterns: *_REPORT, *_ANALYSIS, *_REVIEW, *_FINDINGS with convention exceptions
- Strong content markers include Executive Summary, Findings, Action Items; supporting markers need two hits
Doc Consolidation by the numbers
- 102 all-time installs (skills.sh)
- Ranked #645 of 1,879 Documentation skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/athola/claude-night-market --skill doc-consolidationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 102 |
|---|---|
| repo stars | ★ 325 |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 2, 2026 |
| Repository | athola/claude-night-market ↗ |
What it does
Scan a repo for stray LLM-generated markdown reports and queue them for consolidation into permanent docs.
Files
Table of Contents
- When to Use
- Quick Start
- Two-Phase Workflow
- Phase 1: Triage (Fast Model))
- Phase 2: Execute (Main Model))
- Workflow Details
- Step 1: Candidate Detection
- Step 2: Content Analysis
- Step 3: Destination Routing
- Step 4: Generate Plan
- Source: API_REVIEW_REPORT.md
- Post-Consolidation
- Step 5: Execute Merges
- Fast Model Delegation
- Content Categories
- Merge Strategies
- Intelligent Weave
- Replace Section
- Append with Context
- Create New File
- Integration
- Example Session
- Troubleshooting
- No candidates found
- Low-quality extractions
- Merge conflicts
- Related Skills
Doc Consolidation
Extracts valuable knowledge from ephemeral LLM outputs and merges it into permanent documentation.
When To Use
Use this skill when:
- You have untracked
*_REPORT.mdor*_ANALYSIS.mdfiles from Claude sessions - Git status shows markdown files that shouldn't be committed but contain useful content
- You want to preserve insights from code reviews, refactoring reports, or API audits
- Preparing a PR and need to clean up working artifacts
Do NOT use when:
- Files are already in proper documentation locations
(docs/, skills/)
- Files are intentionally temporary scratch notes
- User explicitly wants to preserve the original report format
- Source files have no extractable value (pure log output)
Formatting
When merging content into permanent documentation, follow Skill(leyline:markdown-formatting) conventions: wrap prose at 80 chars (prefer sentence/clause boundaries), blank lines around headings, ATX headings only, blank line before lists, and reference-style links for long URLs.
Quick Start
/consolidate-docsOr invoke directly:
I have some report files that need consolidating into permanent docs.Two-Phase Workflow
Phase 1: Triage (Fast Model)
Read-only analysis to generate a consolidation plan:
1. Detect candidates - Find untracked markdown files with LLM output markers 2. Analyze content - Extract and categorize valuable sections 3. Route destinations - Match content to existing docs or propose new files 4. Present plan - Show user what will be consolidated and where
Checkpoint: User reviews and approves plan before execution.
Phase 2: Execute (Main Model)
After approval, performs the consolidation:
1. Merge content - Weave into existing docs or create new files 2. Delete sources - Remove ephemeral files after successful merge 3. Generate summary - Report what was created/updated/deleted
Workflow Details
Step 1: Candidate Detection
Load: @modules/candidate-detection.md
Identifies files using:
- Git status (untracked
.mdfiles) - Location (not in standard doc directories)
- Naming (ALL_CAPS non-standard names)
- Content markers (Executive Summary, Findings, Action Items)
Step 2: Content Analysis
Load: @modules/content-analysis.md
For each candidate:
- Extract sections as content chunks
- Categorize: Actionable Items, Decisions, Findings, Metrics, Migration Guides, API Changes
- Score value: high/medium/low
Step 3: Destination Routing
Load: @modules/destination-routing.md
For each valuable chunk:
- Semantic match against existing documentation
- Apply default mappings if no good match
- Determine merge strategy (weave, replace, append, create)
Step 4: Generate Plan
Present consolidation plan to user:
# Consolidation Plan
## Source: API_REVIEW_REPORT.md
| Content | Category | Value | Destination | Action |
|---------|----------|-------|-------------|--------|
| API inventory | Findings | High | docs/api-overview.md | Create |
| Action items | Actionable | High | docs/plans/2025-12-06-api.md | Create |
### Post-Consolidation
- Delete: API_REVIEW_REPORT.md
Proceed with consolidation? [Y/n]Step 5: Execute Merges
Load: @modules/merge-execution.md
After user approval:
- Group operations by destination file
- Apply merge strategies
- Validate results (frontmatter intact, structure preserved)
- Delete source files
- Generate execution summary
Fast Model Delegation
Phase 1 tasks are delegated to haiku-class models for efficiency:
# plugins/sanctum/scripts/consolidation_planner.py handles:
- scan_for_candidates()
- extract_content_chunks()
- categorize_chunks()
- score_value()
- find_semantic_matches()Phase 2 stays on the main model for careful merge execution.
Content Categories
| Category | Description | Default Destination |
|---|---|---|
| Actionable Items | Tasks, TODOs, next steps | docs/plans/YYYY-MM-DD-{topic}.md |
| Decisions Made | Architecture choices | docs/adr/NNNN-{date}-{topic}.md |
| Findings/Insights | Audit results, analysis | Best-match existing doc |
| Metrics/Baselines | Before/after comparisons | docs/benchmarks/ |
| Migration Guides | Step-by-step procedures | docs/migration-guide.md |
| API Changes | Breaking changes, deprecations | CHANGELOG or api docs |
Merge Strategies
Intelligent Weave
Insert content into matching existing sections, preserving style.
Use when: Destination has relevant section, content is additive.
Replace Section
Replace existing section with more detailed new content.
Use when: New content 2x+ more detailed or has later date.
Append with Context
Add new section with date and source reference.
Use when: No matching section, content doesn't fit existing structure.
Create New File
Generate complete new document.
Use when: No suitable destination exists, content warrants standalone doc.
Integration
Works with other sanctum skills:
- git-workspace-review: May suggest consolidation when untracked reports detected
- pr-prep: Warns about untracked analysis files before PR creation
- doc-updates: Can invoke for final polish after consolidation
Example Session
User: I have API_REVIEW_REPORT.md from our review session. Can you consolidate it?
Claude: I'll consolidate the report into permanent documentation.
**Phase 1: Triage**
Scanning for candidates...
- Found: API_REVIEW_REPORT.md (untracked, LLM markers detected)
Analyzing content...
- Extracted 6 content chunks
- High value: API inventory, action items, CLI recommendation
- Medium value: security assessment
- Low value: executive summary (generic)
Routing destinations...
- API inventory → docs/api-overview.md (CREATE)
- Action items → docs/plans/2025-12-06-api-consistency.md (CREATE)
- CLI naming → docs/adr/0002-cli-naming.md (CREATE ADR)
- Security → Already covered in docs/security.md (SKIP)
**Consolidation Plan**
[table shown]
Proceed? [Y/n]
User: Y
Claude: **Phase 2: Executing**
Created: docs/api-overview.md (1,247 bytes)
Created: docs/plans/2025-12-06-api-consistency.md (892 bytes)
Created: docs/adr/0002-2025-12-06-cli-naming.md (634 bytes)
Deleted: API_REVIEW_REPORT.md
Consolidation complete. Review the created files and commit when ready.Troubleshooting
No candidates found
- Check git status for untracked files
- Files may already be in standard locations
- Try explicit file path: "Consolidate API_REVIEW_REPORT.md"
Low-quality extractions
- Source file may lack structured sections
- Content may be too generic to categorize
- Try manual extraction for unstructured reports
Merge conflicts
- Destination file structure changed
- Try APPEND strategy instead of WEAVE
- Manual intervention may be needed
Related Skills
sanctum:doc-updates- General documentation updatessanctum:git-workspace-review- Pre-flight workspace analysissanctum:pr-prep- Pull request preparationimbue:catchup- Understanding recent changes
Candidate Detection Module
Identifies markdown files that are candidates for consolidation.
Detection Signals
Apply signals in priority order. A file is a candidate if it matches any signal.
Signal 1: Git-Untracked Location (Highest Priority)
# Find untracked .md files
git status --porcelain | grep '^??' | grep '\.md$'Exclude standard locations:
docs/- Already permanent documentationskills/- Skill definitionsmodules/- Skill modulescommands/- Slash commandsagents/- Agent definitions.github/- GitHub templates
Exclude standard names:
README.md,READMELICENSE.md,LICENSECONTRIBUTING.mdCHANGELOG.md,HISTORY.mdSECURITY.mdCODE_OF_CONDUCT.md
Signal 2: ALL_CAPS Naming Pattern
Files with ALL_CAPS names that aren't standard conventions:
MATCHES (candidates):
- API_REVIEW_REPORT.md
- REFACTORING_REPORT.md
- MIGRATION_ANALYSIS.md
- AUDIT_FINDINGS.md
- *_REPORT.md
- *_ANALYSIS.md
- *_REVIEW.md
- *_FINDINGS.md
EXCLUDES (not candidates):
- README.md
- LICENSE.md
- CONTRIBUTING.md
- CHANGELOG.md
- SECURITY.md
- CODE_OF_CONDUCT.mdSignal 3: Content Markers
Scan first 100 lines for LLM output markers:
Strong markers (any one = candidate):
**Date**:orDate:at start of line## Executive Summary## Summary(at document start)## Findings## Action Items## Recommendations## Conclusion
Supporting markers (need 2+ to qualify):
- Markdown tables with
|columns ### High Priority/### Medium Priority/### Low Priority- [ ]checkbox lists## 1.numbered top-level sections**Scope**:or**Status**:- Lines starting with status markers
Detection Algorithm
def detect_candidates(repo_path: str) -> list[CandidateFile]:
candidates = []
# Get untracked markdown files
untracked = git_untracked_md_files(repo_path)
for file_path in untracked:
# Skip standard locations
if is_standard_location(file_path):
continue
# Skip standard names
if is_standard_name(file_path):
continue
score = 0
reasons = []
# Check naming pattern
if is_allcaps_nonstandard(file_path):
score += 3
reasons.append("ALL_CAPS non-standard name")
# Check content markers
content = read_first_n_lines(file_path, 100)
strong, supporting = count_content_markers(content)
if strong > 0:
score += 3
reasons.append(f"Strong markers: {strong}")
if supporting >= 2:
score += 2
reasons.append(f"Supporting markers: {supporting}")
# Threshold: score >= 2
if score >= 2:
candidates.append(CandidateFile(
path=file_path,
score=score,
reasons=reasons
))
return sorted(candidates, key=lambda c: c.score, reverse=True)Output Format
## Detected Candidates
| File | Score | Reasons |
|------|-------|---------|
| API_REVIEW_REPORT.md | 6 | ALL_CAPS name, Strong markers: 3, Supporting: 4 |
| REFACTORING_REPORT.md | 5 | ALL_CAPS name, Strong markers: 2 |
| analysis-notes.md | 2 | Supporting markers: 3 |
Proceeding with 3 candidates...Edge Cases
Nested untracked directories
If an entire directory is untracked, scan all .md files within:
git status --porcelain | grep '^??' | while read status path; do
if [ -d "$path" ]; then
find "$path" -name "*.md"
fi
donePartially staged files
Files that are partially staged (MM or AM status) should be flagged for user attention - they may contain mixed committed/uncommitted content.
Renamed/moved files
If git shows a rename (R status), check if the destination is a standard location. If moving TO a standard location, not a candidate.
Validation
Before proceeding, confirm candidates with user:
Found 3 consolidation candidates:
1. **API_REVIEW_REPORT.md** (score: 6)
- ALL_CAPS non-standard name
- Contains: Executive Summary, Findings, Action Items
2. **REFACTORING_REPORT.md** (score: 5)
- ALL_CAPS non-standard name
- Contains: Summary, Conclusion
3. **analysis-notes.md** (score: 2)
- Contains: Multiple tables, checkbox lists
Analyze these files for consolidation? [Y/n/select specific]Content Analysis Module
Extracts and categorizes valuable content from candidate files.
Content Categories
Category Definitions
| Category | Description | Indicators |
|---|---|---|
| Actionable Items | Tasks, TODOs, next steps that require action | Action Items, Next Steps, TODO, - [ ] checkboxes |
| Decisions Made | Architecture choices, tradeoffs, rationale | Decision, Chose, Tradeoff, Rationale, Why we |
| Findings/Insights | Audit results, analysis conclusions, observations | Findings, Observations, Analysis, Discovered, Noted |
| Metrics/Baselines | Quantitative data, before/after, benchmarks | Tables with numbers, Before, After, percentages, Improvement |
| Migration Guides | Step-by-step procedures, how-to instructions | Steps, How to, Migration, numbered lists with commands |
| API Changes | Interface modifications, breaking changes, deprecations | API, Breaking, Deprecated, New endpoint, Removed |
Extraction Process
For each candidate file:
1. Parse structure - Identify sections by headers (##, ###) 2. Extract chunks - Each section becomes a content chunk 3. Categorize - Match chunk to best-fit category 4. Score value - Assess high/medium/low
Value Scoring
High Value
Content that is:
- Specific: Contains concrete names, paths, numbers
- Actionable: Reader can act on it directly
- Unique: Not already documented elsewhere
Examples:
- Specific action items with owners
- Concrete metrics (before: 287 lines, after: 255 lines)
- Explicit decisions with rationale
- Step-by-step procedures that worked
Medium Value
Content that is:
- Somewhat specific: General guidance with some detail
- Reference-worthy: Useful for future lookups
- Partially covered: Extends existing documentation
Examples:
- General recommendations without specifics
- Findings that align with existing docs
- Metrics without clear baseline comparison
Low Value
Content that is:
- Generic: Could apply to any project
- Redundant: Already well-documented elsewhere
- Ephemeral: Only relevant to the moment
Examples:
- Executive summaries (usually boilerplate)
- Generic best practice reminders
- Status statements ("The review is complete")
Chunk Extraction Algorithm
def extract_chunks(content: str) -> list[ContentChunk]:
chunks = []
current_section = None
current_content = []
for line in content.split('\n'):
# New section header
if line.startswith('## '):
if current_section:
chunks.append(make_chunk(current_section, current_content))
current_section = line[3:].strip()
current_content = []
elif line.startswith('### '):
# Subsection - append to current or create new
if current_section:
current_content.append(line)
else:
current_section = line[4:].strip()
current_content = []
else:
current_content.append(line)
# Don't forget last section
if current_section:
chunks.append(make_chunk(current_section, current_content))
return chunks
def make_chunk(header: str, content: list[str]) -> ContentChunk:
text = '\n'.join(content).strip()
category = categorize(header, text)
value = score_value(text, category)
return ContentChunk(
header=header,
content=text,
category=category,
value=value
)Categorization Rules
Match in order (first match wins):
CATEGORY_PATTERNS = {
'actionable': [
r'action\s*items?',
r'next\s*steps?',
r'todo',
r'tasks?',
r'- \[ \]', # Unchecked checkboxes
],
'decisions': [
r'decision',
r'chose|chosen',
r'tradeoff',
r'rationale',
r'why\s+we',
r'approach',
],
'findings': [
r'finding',
r'observation',
r'analysis',
r'discovered',
r'audit',
r'review\s+result',
],
'metrics': [
r'\d+%',
r'before.*after',
r'improvement',
r'reduction',
r'benchmark',
r'\|\s*\d+\s*\|', # Table with numbers
],
'migration': [
r'migration',
r'step\s*\d',
r'how\s+to',
r'procedure',
r'```bash', # Code blocks with commands
],
'api_changes': [
r'api',
r'breaking\s+change',
r'deprecat',
r'endpoint',
r'interface',
],
}Output Format
## Content Analysis: API_REVIEW_REPORT.md
### Extracted Chunks
| # | Section | Category | Value | Size |
|---|---------|----------|-------|------|
| 1 | Executive Summary | findings | low | 234 chars |
| 2 | API Surface Inventory | findings | high | 1,847 chars |
| 3 | Consistency Audit Findings | findings | high | 2,103 chars |
| 4 | Action Items | actionable | high | 1,456 chars |
| 5 | Recommendations | actionable | medium | 892 chars |
| 6 | Conclusion | findings | low | 312 chars |
### High-Value Content (4 chunks)
- API Surface Inventory: Detailed plugin API table
- Consistency Audit Findings: Specific issues with examples
- Action Items: Concrete tasks with priorities
- (included in routing)
### Excluded (Low Value)
- Executive Summary: Generic overview
- Conclusion: Status statement onlySpecial Handling
Tables
Preserve markdown tables intact - they often contain valuable structured data:
def is_table(lines: list[str]) -> bool:
return any('|' in line and line.count('|') >= 2 for line in lines)Code Blocks
Preserve code blocks, especially those showing:
- Configuration examples
- Command sequences
- Before/after code comparisons
Checklists
Preserve checkbox lists - they indicate actionable items:
- [x] Completed item (historical record)
- [ ] Pending item (action needed)Cross-references
Note internal references for destination routing:
# Links like "See also: docs/security.md" suggest destinations
REFERENCE_PATTERN = r'see\s+(?:also:?\s*)?([^\s,]+\.md)'Destination Routing Module
Maps extracted content chunks to appropriate destinations in the documentation.
Routing Strategy
Priority Order
1. Semantic match - Find existing doc that covers the topic 2. Default mapping
- Use category-based default destinations
3. Create new - Only when no suitable destination exists
Preference: Existing Over New
Always prefer merging into existing documentation:
- Keeps documentation consolidated
- Avoids duplicate coverage
- Maintains established structure
Create new files only when:
- Content is substantial (>500 chars of high-value)
- No existing doc covers the topic
- Content warrants standalone treatment
Semantic Matching
Algorithm
def find_semantic_match(chunk: ContentChunk, existing_docs: list[str]) -> str | None:
"""Find best-matching existing document for a content chunk."""
best_match = None
best_score = 0
for doc_path in existing_docs:
doc_content = read_file(doc_path)
score = compute_relevance(chunk, doc_content)
if score > best_score and score >= MATCH_THRESHOLD:
best_match = doc_path
best_score = score
return best_match
def compute_relevance(chunk: ContentChunk, doc_content: str) -> float:
"""Score relevance of chunk to document."""
score = 0.0
# Header matching (highest weight)
doc_headers = extract_headers(doc_content)
if any(similar(chunk.header, h) for h in doc_headers):
score += 0.4
# Keyword overlap
chunk_keywords = extract_keywords(chunk.content)
doc_keywords = extract_keywords(doc_content)
overlap = len(chunk_keywords & doc_keywords) / len(chunk_keywords)
score += overlap * 0.3
# Category alignment
if doc_likely_category(doc_content) == chunk.category:
score += 0.2
# Reference mentions
if chunk mentions doc_path or doc mentions chunk source:
score += 0.1
return score
MATCH_THRESHOLD = 0.5 # Minimum score to consider a matchExisting Doc Discovery
Scan these locations for potential destinations:
DOC_LOCATIONS = [
'docs/',
'docs/plans/',
'docs/adr/',
'README.md',
'CHANGELOG.md',
]
# Plugin-specific locations
PLUGIN_DOC_LOCATIONS = [
'{plugin}/docs/',
'{plugin}/README.md',
]Default Mappings
When semantic matching finds no suitable destination:
| Category | Default Destination | Notes |
|---|---|---|
| Actionable Items | docs/plans/YYYY-MM-DD-{topic}.md | New plan file |
| Decisions Made | docs/adr/NNNN-YYYY-MM-DD-{topic}.md | New ADR |
| Findings/Insights | docs/{topic}.md | New doc or best-effort match |
| Metrics/Baselines | docs/benchmarks.md or inline | Append if exists |
| Migration Guides | docs/migration-guide.md | Append section |
| API Changes | CHANGELOG.md or docs/api.md | Prefer CHANGELOG |
Topic Extraction
Derive topic slug from content:
def extract_topic(chunk: ContentChunk, source_file: str) -> str:
"""Extract topic slug for file naming."""
# Try chunk header first
if chunk.header:
return slugify(chunk.header)
# Try source file name
source_name = Path(source_file).stem
if '_REPORT' in source_name:
return slugify(source_name.replace('_REPORT', ''))
# Fall back to category
return chunk.category
def slugify(text: str) -> str:
"""Convert text to kebab-case slug."""
text = text.lower()
text = re.sub(r'[^a-z0-9]+', '-', text)
text = text.strip('-')
return text[:50] # Max lengthMerge Strategy Selection
For each chunk-destination pair, determine how to merge:
Decision Tree
Is destination a new file?
├── Yes → CREATE_NEW
└── No → Does destination have matching section?
├── Yes → Is new content more detailed?
│ ├── Yes (2x+ detail OR newer date) → REPLACE_SECTION
│ └── No → INTELLIGENT_WEAVE
└── No → APPEND_WITH_CONTEXTStrategy Definitions
CREATE_NEW
- Generate complete new file with frontmatter
- Use appropriate template for category
- Include source attribution
INTELLIGENT_WEAVE
- Find matching section in destination
- Insert content matching existing style
- Preserve bullet/table/prose formatting
REPLACE_SECTION
- Back up existing content (in consolidation log)
- Replace entire section
- Add "Updated: YYYY-MM-DD" marker
APPEND_WITH_CONTEXT
- Add new section at logical location
- Include header with date and source
- Format:
## {Topic} (consolidated from {source}, {date})
Output Format
## Routing Plan
### Source: API_REVIEW_REPORT.md
| Chunk | Destination | Strategy | Rationale |
|-------|-------------|----------|-----------|
| API Surface Inventory | docs/api-overview.md | CREATE_NEW | No existing API docs, substantial content |
| Consistency Findings | docs/architecture.md | APPEND_WITH_CONTEXT | Related topic, no matching section |
| Action Items | docs/plans/2025-12-06-api-consistency.md | CREATE_NEW | Actionable, needs tracking |
| CLI Recommendation | docs/adr/0002-cli-naming.md | CREATE_NEW | Decision warrants ADR |
### Skipped (Low Value)
| Chunk | Reason |
|-------|--------|
| Executive Summary | Generic, low value |
| Conclusion | Status only, no lasting value |
### Destination Summary
- **New files**: 3
- **Updates to existing**: 1
- **Skipped**: 2ADR Generation
For decisions that warrant an ADR:
# ADR-{NNNN}: {Decision Title}
**Date**: {YYYY-MM-DD}
**Status**: Accepted
**Consolidated from**: {source_file}
## Context
{extracted context from source}
## Decision
{extracted decision}
## Consequences
{extracted consequences or "To be determined"}
## References
- Source: {source_file} (consolidated {date})ADR Numbering
def next_adr_number(adr_dir: str) -> int:
"""Find next available ADR number."""
existing = glob(f"{adr_dir}/[0-9][0-9][0-9][0-9]-*.md")
if not existing:
return 1
numbers = [int(Path(p).name[:4]) for p in existing]
return max(numbers) + 1Validation
Before finalizing routing:
1. Check destination exists (for updates) 2. Check write permissions 3. Verify no circular references 4. Confirm ADR numbering is unique
def validate_routing(plan: RoutingPlan) -> list[str]:
"""Return list of validation errors."""
errors = []
for route in plan.routes:
if route.strategy != 'CREATE_NEW':
if not Path(route.destination).exists():
errors.append(f"Destination not found: {route.destination}")
if route.strategy == 'CREATE_NEW':
if Path(route.destination).exists():
errors.append(f"Would overwrite existing: {route.destination}")
return errorsMerge Execution Module
Executes the approved consolidation plan, performing actual file operations.
Execution Order
1. Group by destination - Minimize file I/O 2. Process creates first - New files before updates 3. Process updates - Apply merges to existing files 4. Delete sources - Remove after successful consolidation 5. Generate summary - Report all changes
Pre-Execution Checks
Before any file operations:
def pre_execution_checks(plan: ConsolidationPlan) -> list[str]:
"""Validate plan is safe to execute. Returns errors."""
errors = []
# Check all destinations are writable
for route in plan.routes:
dest_dir = Path(route.destination).parent
if not dest_dir.exists():
# Will create - check parent is writable
if not os.access(dest_dir.parent, os.W_OK):
errors.append(f"Cannot create directory: {dest_dir}")
elif not os.access(route.destination, os.W_OK):
errors.append(f"Cannot write to: {route.destination}")
# Check sources exist and are readable
for source in plan.sources:
if not Path(source).exists():
errors.append(f"Source not found: {source}")
# Check for conflicting operations
destinations = [r.destination for r in plan.routes]
if len(destinations) != len(set(destinations)):
# Multiple chunks going to same file - need ordering
pass # This is fine, handled by grouping
return errorsStrategy Implementations
CREATE_NEW
def execute_create_new(route: Route) -> ExecutionResult:
"""Create a new file with content."""
# validate directory exists
dest_path = Path(route.destination)
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Generate content based on category
if route.chunk.category == 'decisions':
content = generate_adr_content(route)
elif route.chunk.category == 'actionable':
content = generate_plan_content(route)
else:
content = generate_doc_content(route)
# Write file
dest_path.write_text(content)
return ExecutionResult(
destination=route.destination,
action='created',
bytes_written=len(content),
)INTELLIGENT_WEAVE
def execute_intelligent_weave(route: Route) -> ExecutionResult:
"""Insert content into matching section of existing file."""
dest_path = Path(route.destination)
original = dest_path.read_text()
# Find matching section
section_pattern = find_matching_section(original, route.chunk.header)
if not section_pattern:
# Fall back to append
return execute_append_with_context(route)
# Analyze existing style
style = analyze_section_style(original, section_pattern)
# Format new content to match
formatted = format_to_match_style(route.chunk.content, style)
# Insert at appropriate location within section
updated = insert_in_section(original, section_pattern, formatted)
# Validate result
if not validate_markdown(updated):
raise ExecutionError(f"Weave produced invalid markdown for {route.destination}")
dest_path.write_text(updated)
return ExecutionResult(
destination=route.destination,
action='weaved',
section=section_pattern.header,
bytes_added=len(formatted),
)
def analyze_section_style(content: str, section: SectionMatch) -> Style:
"""Determine formatting style of existing section."""
section_content = extract_section_content(content, section)
return Style(
uses_bullets=bool(re.search(r'^[-*]\s', section_content, re.M)),
uses_numbers=bool(re.search(r'^\d+\.\s', section_content, re.M)),
uses_tables=bool(re.search(r'^\|.*\|$', section_content, re.M)),
indent_style=detect_indent(section_content),
has_blank_lines='\n\n' in section_content,
)REPLACE_SECTION
def execute_replace_section(route: Route) -> ExecutionResult:
"""Replace entire section with new content."""
dest_path = Path(route.destination)
original = dest_path.read_text()
# Find section boundaries
section = find_section_boundaries(original, route.target_section)
if not section:
raise ExecutionError(f"Section '{route.target_section}' not found in {route.destination}")
# Log what we're replacing (for rollback if needed)
replaced_content = original[section.start:section.end]
log_replacement(route.destination, route.target_section, replaced_content)
# Build replacement with update marker
replacement = f"{section.header}\n\n"
replacement += f"*Updated: {date.today().isoformat()} (consolidated from {route.source})*\n\n"
replacement += route.chunk.content
# Replace in document
updated = original[:section.start] + replacement + original[section.end:]
dest_path.write_text(updated)
return ExecutionResult(
destination=route.destination,
action='replaced',
section=route.target_section,
bytes_before=len(replaced_content),
bytes_after=len(replacement),
)APPEND_WITH_CONTEXT
def execute_append_with_context(route: Route) -> ExecutionResult:
"""Add new section at end of document."""
dest_path = Path(route.destination)
original = dest_path.read_text()
# Determine section level (match document)
header_level = detect_header_level(original)
# Build new section
new_section = f"\n\n{'#' * header_level} {route.chunk.header}"
new_section += f" (consolidated {date.today().isoformat()})\n\n"
new_section += f"*Source: {route.source}*\n\n"
new_section += route.chunk.content
# Append
updated = original.rstrip() + new_section + "\n"
dest_path.write_text(updated)
return ExecutionResult(
destination=route.destination,
action='appended',
section=route.chunk.header,
bytes_added=len(new_section),
)Source Deletion
After all merges complete successfully:
def delete_sources(plan: ConsolidationPlan, results: list[ExecutionResult]) -> list[str]:
"""Delete source files after successful consolidation."""
# Only delete if ALL operations succeeded
if any(r.status == 'failed' for r in results):
return [] # Don't delete anything
deleted = []
for source in plan.sources:
source_path = Path(source)
if source_path.exists():
source_path.unlink()
deleted.append(source)
return deletedRollback Support
Maintain log for potential rollback:
CONSOLIDATION_LOG = '.consolidation-log.json'
def log_operation(operation: dict):
"""Log operation for potential rollback."""
log_path = Path(CONSOLIDATION_LOG)
if log_path.exists():
log = json.loads(log_path.read_text())
else:
log = {'operations': [], 'timestamp': datetime.now().isoformat()}
log['operations'].append(operation)
log_path.write_text(json.dumps(log, indent=2))
def rollback_last():
"""Rollback most recent consolidation."""
log_path = Path(CONSOLIDATION_LOG)
if not log_path.exists():
raise RollbackError("No consolidation log found")
log = json.loads(log_path.read_text())
# Reverse operations
for op in reversed(log['operations']):
if op['action'] == 'created':
Path(op['destination']).unlink()
elif op['action'] == 'replaced':
restore_section(op['destination'], op['section'], op['original'])
elif op['action'] == 'deleted':
# Cannot restore deleted sources automatically
print(f"WARNING: Cannot restore deleted source: {op['source']}")
log_path.unlink()Execution Summary
Generate detailed summary:
# Consolidation Complete
**Timestamp**: 2025-12-06T14:32:15
**Duration**: 2.3s
## Created Files (3)
| File | Size | Category |
|------|------|----------|
| docs/api-overview.md | 1,847 bytes | findings |
| docs/plans/2025-12-06-api-consistency.md | 1,456 bytes | actionable |
| docs/adr/0002-2025-12-06-cli-naming.md | 634 bytes | decisions |
## Updated Files (1)
| File | Section | Strategy | Change |
|------|---------|----------|--------|
| docs/architecture.md | Consistency | APPEND | +892 bytes |
## Deleted Sources (1)
- ~~API_REVIEW_REPORT.md~~ (deleted)
## Verification Checklist
- [ ] Review created files for accuracy
- [ ] Check weaved content fits naturally
- [ ] Run documentation build (if applicable)
- [ ] Commit changes
**Suggested commit message:**docs: consolidate API review findings
- Created api-overview.md with plugin API inventory
- Created plan for API consistency improvements
- Added ADR for CLI naming convention
- Updated architecture.md with consistency findings
Consolidated from: API_REVIEW_REPORT.md
Error Handling
class ExecutionError(Exception):
"""Error during merge execution."""
pass
def execute_with_recovery(plan: ConsolidationPlan) -> ExecutionSummary:
"""Execute plan with error recovery."""
results = []
try:
# Execute creates
for route in plan.creates:
result = execute_create_new(route)
log_operation(result.to_dict())
results.append(result)
# Execute updates
for route in plan.updates:
if route.strategy == 'INTELLIGENT_WEAVE':
result = execute_intelligent_weave(route)
elif route.strategy == 'REPLACE_SECTION':
result = execute_replace_section(route)
else:
result = execute_append_with_context(route)
log_operation(result.to_dict())
results.append(result)
# Delete sources
deleted = delete_sources(plan, results)
for source in deleted:
log_operation({'action': 'deleted', 'source': source})
return ExecutionSummary(results=results, deleted=deleted, status='success')
except Exception as e:
# Log failure but don't auto-rollback
return ExecutionSummary(
results=results,
status='partial_failure',
error=str(e),
message="Some operations failed. Use rollback if needed."
)Related skills
FAQ
Is Doc Consolidation safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.