📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,424 @@
---
name: docs-audit
description: |
Comprehensive documentation audit against current code state. Checks markdown files for accuracy, link validity, code example correctness, and docstring coverage. Uses efficient discovery to minimize context usage while providing thorough analysis.
context: fork
agent: editor
allowed-tools:
- Bash
- Read
- Write
- Glob
- Grep
- TaskCreate
- TaskUpdate
- TaskList
- TaskGet
metadata:
arg-schema:
path:
type: string
description: Optional path to limit audit scope (e.g., "docs/", "src/")
focus:
type: string
enum: [stale, all, recent]
default: stale
description: Which files to prioritize - stale (default), all, or recently modified
limit:
type: number
default: 10
description: Maximum number of files to analyze deeply (5-20 recommended)
output:
type: string
description: Output path for report. Defaults to .pack/reports/{timestamp}-docs-audit-{sessionShort}.md
multi:
type: boolean
default: false
description: Multi-file mode - creates .pack/reports/{timestamp}-docs-audit-{sessionShort}/ directory
---
# Documentation Audit Skill
Audit documentation files against the current codebase state, checking for accuracy, completeness, and freshness.
## Workflow
### Stage 1: Discovery
Run the discovery script to get a manifest of all markdown files with git metadata:
```bash
bun "$(dirname "$0")/scripts/discover-docs.ts" ${path ? `--path "${path}"` : ""} --limit 50 --sort staleness
```
Parse the JSON output to understand:
- Total documentation files in scope
- Activity status distribution (active/recent/idle/stale/ancient)
- Files with related code changes (potential staleness indicators)
### Stage 2: Prioritization
Select files for deep analysis based on `focus` argument:
| Focus | Strategy |
|-------|----------|
| `stale` | Prioritize files with oldest commits, especially those with recent related code changes |
| `all` | Balanced sampling across activity statuses |
| `recent` | Focus on recently modified docs that may have introduced errors |
Target: Select top `limit` files (default 10) for deep analysis.
### Stage 3: Deep Analysis
For each selected file, perform these checks:
#### 3.1 Correctness (see references/correctness-checklist.md)
- [ ] Code examples use correct imports/require paths
- [ ] Function signatures match current implementation
- [ ] Configuration examples reflect current schema
- [ ] CLI commands and flags are accurate
- [ ] Environment variables mentioned actually exist
#### 3.2 Link Validation
- [ ] Internal markdown links (`[text](./other.md)`) resolve
- [ ] Anchor links (`[text](#section)`) point to existing headings
- [ ] Image references exist
- [ ] External URLs (sample only if many) - note but don't block on these
#### 3.3 Completeness (see references/completeness-checklist.md)
- [ ] Required sections present (varies by doc type)
- [ ] All public exports documented (for API docs)
- [ ] Examples provided for complex features
- [ ] Error handling documented where relevant
### Stage 4: Docstring Coverage
Check TSDoc/JSDoc/docstring coverage for code files related to the documentation:
**TypeScript/JavaScript:**
```bash
# Find exports without TSDoc
grep -rn "^export " --include="*.ts" --include="*.tsx" | head -20
# vs exports with TSDoc (/** precedes export)
grep -B1 "^export " --include="*.ts" --include="*.tsx" | grep -c "/\*\*"
```
**Python:**
```bash
# Find functions/classes without docstrings
grep -rn "^def \|^class " --include="*.py" | head -20
```
**Rust:**
```bash
# Find pub items without doc comments
grep -rn "^pub " --include="*.rs" | head -20
```
**Go:**
```bash
# Find exported funcs without godoc
grep -rn "^func [A-Z]" --include="*.go" | head -20
```
Calculate coverage percentage per language detected.
### Stage 5: Report Generation
First, generate the report path using the helper script:
```bash
REPORT_PATH=$(bun "$(dirname "$0")/scripts/report-path.ts" --session "${CLAUDE_SESSION_ID}" --json)
# Extracts: timestamp, sessionShort, path, timestampISO
```
Write the report to the generated path (e.g., `.pack/reports/202601251900-docs-audit-a7b3c2d1.md`):
```markdown
---
type: docs-audit
generated: {timestampISO}
timestamp: "{timestamp}"
session: "{CLAUDE_SESSION_ID}"
session_short: "{sessionShort}"
scope: {path or "entire repo"}
focus: {focus}
files_analyzed: {count}
files_total: {total}
status: {pass|needs-work|critical}
---
# Documentation Audit Report
**Generated**: {timestamp}
**Session**: `{sessionShort}`
**Scope**: {path or "entire repo"}
**Files analyzed**: {count} / {total}
**Focus**: {focus}
## Summary
| Dimension | Status | Score |
|-----------|--------|-------|
| Correctness | {PASS/NEEDS WORK} | {x}/{y} files |
| Links | {PASS/NEEDS WORK} | {valid}/{total} |
| Docstrings | {GOOD/ACCEPTABLE/POOR} | {x}% |
| Freshness | {CURRENT/STALE} | {stale_count} files |
## Critical Issues (blocking)
Issues that could cause user confusion or errors:
- {file}: {issue description}
## Warnings (should fix)
Non-blocking but should be addressed:
- {file}: {issue description}
## Stale Documentation
Files that may need review (old docs + recent code changes):
- {file}: Last updated {days}d ago, related code changed {code_days}d ago
## Docstring Coverage by Language
| Language | Coverage | Files Checked |
|----------|----------|---------------|
| TypeScript | {x}% | {n} |
| Python | {x}% | {n} |
## Recommendations
1. {Prioritized recommendation}
2. {Next recommendation}
```
## Report Output
### Path Generation & Scaffolding
Use the `report-path.ts` helper script to generate paths and scaffold directories:
```bash
# Get just the path
bun scripts/report-path.ts --session "${CLAUDE_SESSION_ID}"
# → .pack/reports/202601251900-docs-audit-a7b3c2d1.md
# Scaffold the directory structure (creates .pack/reports/)
bun scripts/report-path.ts --scaffold --session "${CLAUDE_SESSION_ID}"
# Multi-file mode: scaffold with placeholder files
bun scripts/report-path.ts --scaffold --multi --session "${CLAUDE_SESSION_ID}"
# Creates:
# .pack/reports/202601251900-docs-audit/
# .pack/reports/202601251900-docs-audit/summary.md
# .pack/reports/202601251900-docs-audit/markdown-docs.md
# .pack/reports/202601251900-docs-audit/docstrings.md
# .pack/reports/202601251900-docs-audit/recommendations.md
# .pack/reports/202601251900-docs-audit/meta.json
# Get all components as JSON (includes scaffolded paths if --scaffold used)
bun scripts/report-path.ts --scaffold --multi --session "${CLAUDE_SESSION_ID}" --json
```
### Default Location
Reports are written to `.pack/reports/` with frontloaded timestamp:
```
.pack/reports/202601251900-docs-audit-a7b3c2d1.md # Single file (with session)
.pack/reports/202601251900-docs-audit.md # Single file (no session)
.pack/reports/202601251900-docs-audit/ # Multi-file (no session in dir name)
```
**Filename patterns:**
- **Single-file**: `{timestamp}-docs-audit-{sessionShort}.md` (session for parallel disambiguation)
- **Multi-file**: `{timestamp}-docs-audit/` (session tracked in frontmatter inside files)
### Multi-File Mode
For comprehensive audits covering different documentation types, use `--multi`:
```
.pack/reports/202601251900-docs-audit/
├── summary.md # Overall findings + links to other reports
├── markdown-docs.md # docs/, README, etc.
├── docstrings.md # TSDoc/JSDoc/docstring coverage
├── recommendations.md # Prioritized actionable recommendations
└── meta.json # Session metadata (structured, machine-readable)
```
Each file includes frontmatter with full session ID for traceability.
### Frontmatter Schema
All report artifacts include YAML frontmatter for searchability:
```yaml
---
type: docs-audit # Report type (searchable)
generated: 2026-01-25T19:00:00Z
timestamp: "202601251900"
session: abc123-def456... # Full session ID
session_short: a7b3c2d1 # First 8 chars (matches filename)
scope: docs/ # Audit scope
focus: stale # Focus strategy used
files_analyzed: 10
files_total: 47
status: needs-work # pass | needs-work | critical
---
```
**Why frontmatter:**
- Grep/ripgrep searchable (`rg "session: abc123"`)
- Tooling can parse and aggregate reports
- Enables filtering by status, scope, date range
- Parallel agent runs are distinguishable by session
### Session ID for Parallel Agents
**Single-file mode** uses session suffix for parallel disambiguation:
```
.pack/reports/202601251900-docs-audit-a7b3c2d1.md # Agent 1
.pack/reports/202601251900-docs-audit-f8e9d0c1.md # Agent 2
.pack/reports/202601251900-docs-audit-12345678.md # Agent 3
```
**Multi-file mode** relies on timestamps (coordinated audits typically don't run in parallel). Session is tracked inside each file's frontmatter for traceability.
### Custom Output
Override the default location (session ID still included in frontmatter):
```
/docs-audit --output docs/audits/latest.md
/docs-audit --multi --output .pack/reports/202601-quarterly/
```
Note: Custom filenames don't auto-include session ID prefix - use frontmatter for tracking.
### Output Behavior
1. **Create directory** if it doesn't exist
2. **Write report(s)** with session ID embedded
3. **Print summary** to conversation (critical issues + file path)
4. **Return path** so user can open/commit the report
### Git Considerations
The default `.pack/reports/` location:
- Should be gitignored for ephemeral reports
- Can be selectively committed for audit history
- Keeps reports separate from actual documentation
## Context Efficiency
This skill uses `context: fork` to run in isolation. The token budget strategy:
| Stage | Token Target | Strategy |
|-------|--------------|----------|
| Discovery | ~200-500 | Script output is compact JSON |
| Prioritization | ~100 | Selection logic only |
| Deep Analysis | ~500-2000/file | Read only selected files |
| Docstring Check | ~500 | Grep summaries, not full files |
| Report | ~1000 | Structured output |
**Total target**: 15-30k tokens for a typical audit.
## Task Management
Use task tools (`TaskCreate`, `TaskUpdate`, `TaskList`) to track progress through stages. Tasks survive context compaction and allow resumption if the audit is interrupted.
### Initial Task Setup
After discovery, create tasks for the audit stages:
```
TaskCreate:
subject: "Run docs-audit discovery"
activeForm: "Running discovery script"
description: "Execute discover-docs.ts, parse manifest, identify {n} files in scope"
TaskCreate:
subject: "Analyze {n} priority docs"
activeForm: "Analyzing documentation"
description: "Deep analysis of top {limit} files for correctness, links, completeness"
TaskCreate:
subject: "Check docstring coverage"
activeForm: "Checking docstring coverage"
description: "Grep exports vs documented exports per detected language"
TaskCreate:
subject: "Generate audit report"
activeForm: "Generating report"
description: "Compile findings into .pack/reports/{timestamp}-docs-audit-{sessionShort}.md"
```
### Progress Tracking
Update tasks as you work:
1. **Before starting a stage**`TaskUpdate` with `status: in_progress`
2. **After completing a stage**`TaskUpdate` with `status: completed`, update description with key findings
3. **If issues found**`TaskCreate` follow-up tasks for fixes
### State Persistence
Before context approaches limit, update task descriptions with checkpoint data:
```
TaskUpdate:
taskId: "2"
description: |
[CHECKPOINT] Analyzed 7/10 docs.
Critical: 2 broken imports in api.md (lines 45, 89)
Warnings: 3 stale files (config.md, setup.md, advanced.md)
Remaining: config.md, setup.md, advanced.md
```
This ensures findings survive compaction even if the stage isn't complete.
### Resumption
If context resets mid-audit:
1. `TaskList` to see current state
2. `TaskGet` on `in_progress` task to read checkpoint data
3. Skip completed stages
4. Resume from checkpoint, don't re-analyze completed files
5. Reference persisted findings in final report
## Handling Edge Cases
**No documentation found:**
Report "No markdown files found in {scope}. Consider adding documentation for your project."
**Very large repos (100+ docs):**
- Stick to `limit` parameter strictly
- Focus on highest-staleness files
- Note total count in report for context
**Non-git repos:**
- Skip git-based metadata (SHA, author, etc.)
- Use file modification times as fallback
- Note "Git metadata unavailable" in report
**Mixed language repos:**
- Detect languages from file extensions
- Report coverage per detected language
- Skip languages with no source files
## Example Invocations
```
/docs-audit # Single report to .pack/reports/
/docs-audit --path docs/ # Scope to docs/ directory
/docs-audit --focus all --limit 20 # Analyze 20 files across all statuses
/docs-audit --multi # Multi-file mode with separate reports
/docs-audit --output docs/audits/latest.md # Custom output location
/docs-audit --multi --path src/ # Multi-file audit of src/ docs
```
@@ -0,0 +1,192 @@
# Documentation Completeness Checklist
Use this checklist when auditing documentation for coverage and required content.
## Export Coverage
### TypeScript/JavaScript
Every public export should have documentation:
```typescript
// ✅ Documented export
/**
* Processes user input and returns validated result.
* @param input - Raw user input string
* @returns Validated and sanitized input
* @throws {ValidationError} If input fails validation
*/
export function processInput(input: string): ValidatedInput { ... }
// ❌ Undocumented export
export function processInput(input: string): ValidatedInput { ... }
```
**Check coverage:**
```bash
# Count exports
EXPORTS=$(grep -c "^export " src/**/*.ts)
# Count documented exports (/** before export)
DOCUMENTED=$(grep -B1 "^export " src/**/*.ts | grep -c "/\*\*")
# Coverage = DOCUMENTED / EXPORTS * 100
```
### Python
Every public function/class should have a docstring:
```python
# ✅ Documented
def process_input(input: str) -> ValidatedInput:
"""Process user input and return validated result.
Args:
input: Raw user input string
Returns:
Validated and sanitized input
Raises:
ValidationError: If input fails validation
"""
...
# ❌ Undocumented
def process_input(input: str) -> ValidatedInput:
...
```
### Rust
Every public item should have doc comments:
```rust
// ✅ Documented
/// Processes user input and returns validated result.
///
/// # Arguments
/// * `input` - Raw user input string
///
/// # Returns
/// Validated and sanitized input
///
/// # Errors
/// Returns `ValidationError` if input fails validation
pub fn process_input(input: &str) -> Result<ValidatedInput, ValidationError> { ... }
// ❌ Undocumented
pub fn process_input(input: &str) -> Result<ValidatedInput, ValidationError> { ... }
```
### Go
Every exported function should have a godoc comment:
```go
// ✅ Documented
// ProcessInput processes user input and returns validated result.
// It returns a ValidationError if input fails validation.
func ProcessInput(input string) (ValidatedInput, error) { ... }
// ❌ Undocumented
func ProcessInput(input string) (ValidatedInput, error) { ... }
```
## Required Sections by Document Type
### README.md
- [ ] **Title** - Clear project name
- [ ] **Description** - What it does (1-2 sentences)
- [ ] **Installation** - How to install/setup
- [ ] **Quick Start** - Minimal working example
- [ ] **Usage** - Basic usage patterns
- [ ] **License** - License type or link
**Nice to have:**
- [ ] Badges (build status, version, etc.)
- [ ] Table of contents (for long READMEs)
- [ ] Contributing guidelines or link
- [ ] Changelog or link
### API Reference
- [ ] **Overview** - What the API does
- [ ] **Authentication** - How to authenticate
- [ ] **Base URL** - API endpoint base
- [ ] **Endpoints** - All public endpoints documented
- [ ] **Request/Response** - Schemas for each endpoint
- [ ] **Errors** - Common error codes and meanings
### Configuration Reference
- [ ] **Overview** - What can be configured
- [ ] **File Location** - Where config lives
- [ ] **Format** - JSON, YAML, TOML, etc.
- [ ] **All Options** - Each config key documented
- [ ] **Defaults** - Default values listed
- [ ] **Examples** - Working config examples
### CLI Reference
- [ ] **Installation** - How to install
- [ ] **Commands** - All commands documented
- [ ] **Options** - Global and command-specific options
- [ ] **Examples** - Common usage examples
- [ ] **Exit Codes** - What different exit codes mean
### Contributing Guide
- [ ] **Setup** - Development environment setup
- [ ] **Workflow** - How to submit changes
- [ ] **Standards** - Code style, testing requirements
- [ ] **Review Process** - What to expect
### Changelog
- [ ] **Version Numbers** - Semantic versioning
- [ ] **Dates** - Release dates
- [ ] **Categories** - Added, Changed, Fixed, Removed
- [ ] **Migration Notes** - For breaking changes
## Cross-Reference Completeness
### Internal Links
- [ ] All mentioned features link to their docs
- [ ] Related concepts are cross-linked
- [ ] No dead internal links
### External Links
- [ ] Dependencies link to their docs
- [ ] Standards link to specifications
- [ ] Tools link to official sites
## Example Completeness
### Code Examples Should Include
- [ ] Necessary imports
- [ ] Variable declarations with types
- [ ] Error handling (where appropriate)
- [ ] Expected output (for non-obvious cases)
### Example Types Needed
- [ ] **Minimal** - Simplest possible usage
- [ ] **Typical** - Common real-world usage
- [ ] **Advanced** - Complex scenarios (if applicable)
- [ ] **Edge Cases** - Unusual but valid inputs
## Accessibility
- [ ] **Alt text** - Images have descriptive alt text
- [ ] **Headings** - Proper heading hierarchy (h1 > h2 > h3)
- [ ] **Code blocks** - Language specified for syntax highlighting
- [ ] **Tables** - Headers on tables
## Severity Classification
| Severity | Criteria | Example |
|----------|----------|---------|
| **Critical** | Core functionality undocumented | No installation instructions, main API undocumented |
| **High** | Important features undocumented | Missing error handling docs, no config reference |
| **Medium** | Nice-to-have sections missing | No contributing guide, missing advanced examples |
| **Low** | Polish items | Missing badges, no table of contents |
@@ -0,0 +1,131 @@
# Documentation Correctness Checklist
Use this checklist when auditing documentation for accuracy against the current codebase.
## Code Examples
### Import Statements
- [ ] Import paths resolve to existing files
- [ ] Named imports match actual exports
- [ ] Package names match `package.json` / `Cargo.toml` / `pyproject.toml`
- [ ] Relative vs absolute imports are correct for the context
**How to verify:**
```bash
# Extract import from doc, check if file exists
grep -E "^import|^from|^require" {doc_file} | head -5
# Then verify each path exists
```
### Function Signatures
- [ ] Function names exist in codebase
- [ ] Parameter names match implementation
- [ ] Parameter types are accurate
- [ ] Return types are accurate
- [ ] Optional parameters marked correctly
**How to verify:**
```bash
# Find function definition in code
grep -rn "function {name}\|{name} = \|def {name}\|fn {name}" --include="*.ts" --include="*.py" --include="*.rs"
```
### Configuration Examples
- [ ] Config keys exist in schema/types
- [ ] Default values match implementation
- [ ] Required vs optional fields accurate
- [ ] Value types (string, number, boolean) correct
**How to verify:**
```bash
# Find config type/interface
grep -rn "interface.*Config\|type.*Config\|Config = " --include="*.ts"
```
## CLI Documentation
### Commands
- [ ] Command names are correct
- [ ] Subcommands exist
- [ ] Command descriptions accurate
### Flags/Options
- [ ] Flag names (short and long) correct
- [ ] Flag descriptions accurate
- [ ] Default values documented correctly
- [ ] Required flags marked as such
**How to verify:**
```bash
# Run help command
{cli} --help
{cli} {subcommand} --help
```
## API Documentation
### Endpoints
- [ ] HTTP methods correct (GET, POST, etc.)
- [ ] URL paths accurate
- [ ] Query parameters documented
- [ ] Request body schema matches implementation
- [ ] Response schema matches implementation
- [ ] Status codes documented
**How to verify:**
```bash
# Find route definitions
grep -rn "app.get\|app.post\|router\." --include="*.ts" --include="*.js"
# Or for OpenAPI
cat openapi.yaml | grep "paths:" -A 100
```
### Authentication
- [ ] Auth methods accurate (Bearer, API key, etc.)
- [ ] Required headers documented
- [ ] Error responses for auth failures documented
## Environment Variables
- [ ] Variable names match actual usage
- [ ] Descriptions accurate
- [ ] Required vs optional clearly marked
- [ ] Example values are realistic (not revealing secrets)
**How to verify:**
```bash
# Find env var usage
grep -rn "process.env\|os.environ\|env::" --include="*.ts" --include="*.py" --include="*.rs"
# Or check .env.example
cat .env.example
```
## Error Messages
- [ ] Documented errors actually thrown by code
- [ ] Error codes/types match implementation
- [ ] Troubleshooting steps are accurate
**How to verify:**
```bash
# Find error definitions
grep -rn "throw new\|raise \|Error::" --include="*.ts" --include="*.py" --include="*.rs"
```
## Version-Specific Features
- [ ] Features available in documented version
- [ ] Deprecated features marked
- [ ] Breaking changes noted with versions
- [ ] Minimum version requirements accurate
## Severity Classification
When an issue is found, classify it:
| Severity | Criteria | Example |
|----------|----------|---------|
| **Critical** | Will cause errors if user follows docs | Wrong import path, non-existent function |
| **High** | Will cause confusion or unexpected behavior | Wrong default value, missing required param |
| **Medium** | Incomplete but not wrong | Missing optional parameters, outdated example |
| **Low** | Cosmetic or minor | Typo in description, suboptimal example |
@@ -0,0 +1,366 @@
#!/usr/bin/env bun
/**
* discover-docs.ts - Markdown documentation discovery with git metadata
*
* Extracts a compact manifest of all markdown files with git history,
* activity status, and related code file tracking for staleness detection.
*
* Usage:
* ./discover-docs.ts # All markdown files
* ./discover-docs.ts --path src/ # Specific directory
* ./discover-docs.ts --limit 20 # Limit results
* ./discover-docs.ts --sort staleness # Sort by staleness (default)
* ./discover-docs.ts --sort alpha # Sort alphabetically
* ./discover-docs.ts --format json # JSON output (default)
* ./discover-docs.ts --format text # Human-readable output
*/
import { $ } from "bun";
import { parseArgs } from "util";
import { stat } from "node:fs/promises";
/**
* Documentation file with git metadata.
*/
interface DocFile {
/** File path relative to repo root */
path: string;
/** Short SHA of last commit touching this file */
lastCommitSha: string;
/** ISO date of last commit */
lastCommitDate: string;
/** Author name of last commit */
lastAuthor: string;
/** Days since last modification */
daysAgo: number;
/** Activity classification based on age */
activityStatus: "active" | "recent" | "idle" | "stale" | "ancient";
/** Line count */
lines: number;
/** File size in bytes */
bytes: number;
/** Code files modified in same commits */
relatedCodeFiles: string[];
}
/**
* Discovery manifest with all markdown files.
*/
interface Manifest {
/** Generation timestamp */
generated: string;
/** Repository root path */
repoRoot: string;
/** Whether this is a git repository */
isGitRepo: boolean;
/** Total files discovered */
totalFiles: number;
/** Documentation files with metadata */
files: DocFile[];
}
// Activity status thresholds (days)
const ACTIVITY_THRESHOLDS = {
active: 7,
recent: 30,
idle: 90,
stale: 365,
// ancient: > 365
} as const;
/**
* Gets activity status classification based on days since last modification.
* @param daysAgo - Days since last modification
* @returns Activity status classification
*/
function getActivityStatus(daysAgo: number): DocFile["activityStatus"] {
if (daysAgo < ACTIVITY_THRESHOLDS.active) return "active";
if (daysAgo < ACTIVITY_THRESHOLDS.recent) return "recent";
if (daysAgo < ACTIVITY_THRESHOLDS.idle) return "idle";
if (daysAgo < ACTIVITY_THRESHOLDS.stale) return "stale";
return "ancient";
}
function daysBetween(date1: Date, date2: Date): number {
const msPerDay = 1000 * 60 * 60 * 24;
return Math.floor((date2.getTime() - date1.getTime()) / msPerDay);
}
async function isGitRepo(): Promise<boolean> {
try {
await $`git rev-parse --is-inside-work-tree`.quiet();
return true;
} catch {
return false;
}
}
async function getRepoRoot(): Promise<string> {
try {
const result = await $`git rev-parse --show-toplevel`.quiet();
return result.text().trim();
} catch {
return process.cwd();
}
}
async function findMarkdownFiles(searchPath?: string): Promise<string[]> {
const pattern = searchPath
? `${searchPath.replace(/\/$/, "")}/**/*.md*`
: "**/*.md*";
try {
// Use git ls-files if in a git repo (respects .gitignore)
const result = await $`git ls-files --cached --others --exclude-standard "${pattern}"`.quiet();
return result
.text()
.trim()
.split("\n")
.filter((f) => f.length > 0);
} catch {
// Fallback to find command
const result = await $`find ${searchPath || "."} -name "*.md" -o -name "*.mdx" 2>/dev/null`.quiet();
return result
.text()
.trim()
.split("\n")
.filter((f) => f.length > 0);
}
}
async function getGitInfo(
filePath: string
): Promise<Pick<DocFile, "lastCommitSha" | "lastCommitDate" | "lastAuthor" | "daysAgo">> {
try {
// Get last commit info: short SHA, ISO date, author name
const result = await $`git log -1 --format="%h|%aI|%an" -- "${filePath}"`.quiet();
const output = result.text().trim();
if (!output) {
// File not yet committed
return {
lastCommitSha: "uncommitted",
lastCommitDate: new Date().toISOString(),
lastAuthor: "unknown",
daysAgo: 0,
};
}
const [sha, date, author] = output.split("|");
const commitDate = new Date(date);
const daysAgo = daysBetween(commitDate, new Date());
return {
lastCommitSha: sha,
lastCommitDate: date,
lastAuthor: author,
daysAgo,
};
} catch {
return {
lastCommitSha: "unknown",
lastCommitDate: new Date().toISOString(),
lastAuthor: "unknown",
daysAgo: 0,
};
}
}
async function getFileStats(filePath: string): Promise<Pick<DocFile, "lines" | "bytes">> {
try {
const stats = await stat(filePath);
const bytes = stats.size;
// Count lines
const file = Bun.file(filePath);
const content = await file.text();
const lines = content.split("\n").length;
return { lines, bytes };
} catch {
return { lines: 0, bytes: 0 };
}
}
async function getRelatedCodeFiles(docPath: string): Promise<string[]> {
try {
// Find code files that have been modified in the same commits as this doc
// Look at last 10 commits that touched this file
const result = await $`git log -10 --format="%H" -- "${docPath}"`.quiet();
const commits = result.text().trim().split("\n").filter(Boolean);
if (commits.length === 0) return [];
const codeExtensions = [
".ts",
".tsx",
".js",
".jsx",
".py",
".rs",
".go",
".java",
".rb",
".c",
".cpp",
".h",
];
const relatedFiles = new Set<string>();
for (const commit of commits.slice(0, 5)) {
// Limit to 5 commits for speed
try {
const filesResult = await $`git diff-tree --no-commit-id --name-only -r ${commit}`.quiet();
const files = filesResult.text().trim().split("\n");
for (const file of files) {
if (file !== docPath && codeExtensions.some((ext) => file.endsWith(ext))) {
relatedFiles.add(file);
}
}
} catch {
// Skip this commit if we can't get its files
}
}
return Array.from(relatedFiles).slice(0, 10); // Limit to 10 related files
} catch {
return [];
}
}
async function analyzeFile(filePath: string, useGit: boolean): Promise<DocFile> {
const [gitInfo, fileStats, relatedCodeFiles] = await Promise.all([
useGit ? getGitInfo(filePath) : Promise.resolve({
lastCommitSha: "n/a",
lastCommitDate: new Date().toISOString(),
lastAuthor: "n/a",
daysAgo: 0,
}),
getFileStats(filePath),
useGit ? getRelatedCodeFiles(filePath) : Promise.resolve([]),
]);
return {
path: filePath,
...gitInfo,
activityStatus: getActivityStatus(gitInfo.daysAgo),
...fileStats,
relatedCodeFiles,
};
}
function sortFiles(files: DocFile[], sortBy: "staleness" | "alpha"): DocFile[] {
if (sortBy === "alpha") {
return files.sort((a, b) => a.path.localeCompare(b.path));
}
// Sort by staleness (most stale first)
return files.sort((a, b) => b.daysAgo - a.daysAgo);
}
function formatText(manifest: Manifest): string {
const lines: string[] = [
`Documentation Audit Discovery`,
`Generated: ${manifest.generated}`,
`Repo: ${manifest.repoRoot}`,
`Git: ${manifest.isGitRepo ? "yes" : "no"}`,
`Total files: ${manifest.totalFiles}`,
``,
`Files:`,
``,
];
const statusEmoji: Record<DocFile["activityStatus"], string> = {
active: "🟢",
recent: "🟡",
idle: "🟠",
stale: "🔴",
ancient: "⚫",
};
for (const file of manifest.files) {
lines.push(`${statusEmoji[file.activityStatus]} ${file.path}`);
lines.push(` SHA: ${file.lastCommitSha} | ${file.daysAgo}d ago | ${file.lastAuthor}`);
lines.push(` Size: ${file.lines} lines, ${file.bytes} bytes`);
if (file.relatedCodeFiles.length > 0) {
lines.push(` Related: ${file.relatedCodeFiles.slice(0, 3).join(", ")}${file.relatedCodeFiles.length > 3 ? "..." : ""}`);
}
lines.push(``);
}
return lines.join("\n");
}
// Main
async function main() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
path: { type: "string", short: "p" },
limit: { type: "string", short: "l" },
sort: { type: "string", short: "s", default: "staleness" },
format: { type: "string", short: "f", default: "json" },
help: { type: "boolean", short: "h" },
},
allowPositionals: true,
});
if (values.help) {
console.log(`
discover-docs.ts - Markdown documentation discovery
Usage:
./discover-docs.ts [options]
Options:
--path, -p <dir> Search in specific directory
--limit, -l <n> Limit number of results
--sort, -s <by> Sort by: staleness (default), alpha
--format, -f <fmt> Output format: json (default), text
--help, -h Show this help
`);
process.exit(0);
}
const useGit = await isGitRepo();
const repoRoot = await getRepoRoot();
const files = await findMarkdownFiles(values.path);
// Analyze files in parallel (batched for large repos)
const batchSize = 20;
const docFiles: DocFile[] = [];
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const results = await Promise.all(batch.map((f) => analyzeFile(f, useGit)));
docFiles.push(...results);
}
// Sort
const sortBy = (values.sort === "alpha" ? "alpha" : "staleness") as "staleness" | "alpha";
const sorted = sortFiles(docFiles, sortBy);
// Limit
const limit = values.limit ? parseInt(values.limit, 10) : sorted.length;
const limited = sorted.slice(0, limit);
const manifest: Manifest = {
generated: new Date().toISOString(),
repoRoot,
isGitRepo: useGit,
totalFiles: files.length,
files: limited,
};
if (values.format === "text") {
console.log(formatText(manifest));
} else {
console.log(JSON.stringify(manifest, null, 2));
}
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});
@@ -0,0 +1,311 @@
#!/usr/bin/env bun
/**
* report-path.ts - Generate report paths with timestamp and session ID
*
* Constructs standardized report filenames following the pattern:
* {timestamp}-{type}-{sessionShort}.md
* {timestamp}-{type}/ (for multi-file)
*
* Usage:
* ./report-path.ts # Uses CLAUDE_SESSION_ID env var
* ./report-path.ts --session abc123-def456 # Explicit session ID
* ./report-path.ts --type docs-audit # Report type (default: docs-audit)
* ./report-path.ts --multi # Output directory path instead of file
* ./report-path.ts --base .pack/reports # Base directory (default: .pack/reports)
* ./report-path.ts --json # Output as JSON with all components
* ./report-path.ts --scaffold # Create directory structure
* ./report-path.ts --scaffold --multi # Scaffold multi-file with placeholders
*/
import { parseArgs } from "util";
import { mkdir, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
/**
* Components of a report path.
*/
interface ReportPathComponents {
/** Timestamp as YYYYMMDDhhmm */
timestamp: string;
/** ISO 8601 timestamp for frontmatter */
timestampISO: string;
/** Report type (e.g., "docs-audit") */
type: string;
/** Full session ID (or empty) */
sessionFull: string;
/** First 8 chars of session ID (or empty) */
sessionShort: string;
/** Just the filename */
filename: string;
/** Full path including base */
path: string;
/** Whether this is a directory mode report */
isMulti: boolean;
}
/**
* Gets current timestamp in both formatted and ISO formats.
* @returns Object with formatted (YYYYMMDDhhmm) and ISO timestamps
*/
function getTimestamp(): { formatted: string; iso: string } {
const now = new Date();
const formatted = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, "0"),
String(now.getDate()).padStart(2, "0"),
String(now.getHours()).padStart(2, "0"),
String(now.getMinutes()).padStart(2, "0"),
].join("");
return {
formatted,
iso: now.toISOString(),
};
}
/**
* Truncates session ID to first 8 characters.
* @param sessionId - Full session ID
* @returns First 8 characters of session ID
*/
function getShortSessionId(sessionId: string): string {
if (!sessionId) return "";
// Handle UUIDs (take first segment) or just first 8 chars
const firstSegment = sessionId.split("-")[0];
return firstSegment.length >= 8 ? firstSegment.slice(0, 8) : firstSegment;
}
/**
* Builds a filename from components.
* @param timestamp - Formatted timestamp
* @param type - Report type
* @param sessionShort - Short session ID
* @param isMulti - Whether this is a multi-file report
* @returns Filename or directory name
*/
function buildFilename(
timestamp: string,
type: string,
sessionShort: string,
isMulti: boolean
): string {
// Multi-file mode: no session in directory name (session tracked in frontmatter)
// Single-file mode: session suffix for parallel disambiguation
if (isMulti) {
return `${timestamp}-${type}`;
}
const parts = [timestamp, type];
if (sessionShort) {
parts.push(sessionShort);
}
return `${parts.join("-")}.md`;
}
// Multi-file report structure
const MULTI_FILE_STRUCTURE = [
{ name: "summary.md", description: "Overall findings and links to other reports" },
{ name: "markdown-docs.md", description: "Analysis of docs/, README, etc." },
{ name: "docstrings.md", description: "TSDoc/JSDoc/docstring coverage" },
{ name: "recommendations.md", description: "Prioritized actionable recommendations" },
{ name: "meta.json", description: "Session metadata (structured)" },
] as const;
/**
* Generates YAML frontmatter for report file.
* @param components - Report path components
* @param fileType - Type of file within report
* @returns Frontmatter string
*/
function generateFrontmatter(components: ReportPathComponents, fileType: string): string {
return `---
type: ${components.type}
file_type: ${fileType}
generated: ${components.timestampISO}
timestamp: "${components.timestamp}"
session: "${components.sessionFull}"
session_short: "${components.sessionShort}"
status: pending
---
`;
}
/**
* Generates meta.json content for multi-file reports.
* @param components - Report path components
* @returns JSON string
*/
function generateMetaJson(components: ReportPathComponents): string {
return JSON.stringify(
{
type: components.type,
generated: components.timestampISO,
timestamp: components.timestamp,
session: components.sessionFull,
session_short: components.sessionShort,
path: components.path,
status: "pending",
files: MULTI_FILE_STRUCTURE.map((f) => f.name),
},
null,
2
);
}
/**
* Creates directory structure and placeholder files for report.
* @param components - Report path components
* @returns Array of created paths
*/
async function scaffoldReport(components: ReportPathComponents): Promise<string[]> {
const created: string[] = [];
if (components.isMulti) {
// Create directory and placeholder files
await mkdir(components.path, { recursive: true });
created.push(components.path);
for (const file of MULTI_FILE_STRUCTURE) {
const filePath = `${components.path}/${file.name}`;
if (!existsSync(filePath)) {
let content: string;
if (file.name.endsWith(".json")) {
// JSON files get structured metadata
content = generateMetaJson(components);
} else {
// Markdown files get frontmatter + placeholder
const fileType = file.name.replace(".md", "");
const frontmatter = generateFrontmatter(components, fileType);
content = `${frontmatter}# ${file.description}\n\n<!-- TODO: Populate during audit -->\n`;
}
await writeFile(filePath, content);
created.push(filePath);
}
}
} else {
// Create base directory only
const baseDir = components.path.substring(0, components.path.lastIndexOf("/"));
await mkdir(baseDir, { recursive: true });
created.push(baseDir);
}
return created;
}
/**
* Generates report path components from options.
* @param options - Report path options
* @returns Report path components
*/
function generateReportPath(options: {
session?: string;
type?: string;
base?: string;
multi?: boolean;
}): ReportPathComponents {
const sessionFull = options.session || process.env.CLAUDE_SESSION_ID || "";
const sessionShort = getShortSessionId(sessionFull);
const type = options.type || "docs-audit";
const base = options.base || ".pack/reports";
const isMulti = options.multi || false;
const { formatted: timestamp, iso: timestampISO } = getTimestamp();
const filename = buildFilename(timestamp, type, sessionShort, isMulti);
const path = `${base}/${filename}`;
return {
timestamp,
timestampISO,
type,
sessionFull,
sessionShort,
filename,
path,
isMulti,
};
}
// Main
async function main() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
session: { type: "string", short: "s" },
type: { type: "string", short: "t", default: "docs-audit" },
base: { type: "string", short: "b", default: ".pack/reports" },
multi: { type: "boolean", short: "m" },
scaffold: { type: "boolean" },
json: { type: "boolean", short: "j" },
help: { type: "boolean", short: "h" },
},
allowPositionals: true,
});
if (values.help) {
console.log(`
report-path.ts - Generate report paths with timestamp and session ID
Usage:
./report-path.ts [options]
Options:
--session, -s <id> Session ID (defaults to CLAUDE_SESSION_ID env var)
--type, -t <type> Report type (default: docs-audit)
--base, -b <dir> Base directory (default: .pack/reports)
--multi, -m Directory mode (no session suffix, files inside)
--scaffold Create directory structure (with placeholders for --multi)
--json, -j Output as JSON with all components
--help, -h Show this help
Examples:
./report-path.ts
# Output: .pack/reports/202601251900-docs-audit.md
./report-path.ts --session abc12345-def-ghi
# Output: .pack/reports/202601251900-docs-audit-abc12345.md
./report-path.ts --multi
# Output: .pack/reports/202601251900-docs-audit
./report-path.ts --scaffold --multi --session abc123
# Creates: .pack/reports/202601251900-docs-audit/
# .pack/reports/202601251900-docs-audit/summary.md
# .pack/reports/202601251900-docs-audit/markdown-docs.md
# ...
./report-path.ts --json
# Output: {"timestamp":"202601251900","sessionShort":"abc12345",...}
`);
process.exit(0);
}
const result = generateReportPath({
session: values.session,
type: values.type,
base: values.base,
multi: values.multi,
});
if (values.scaffold) {
const created = await scaffoldReport(result);
if (values.json) {
console.log(JSON.stringify({ ...result, scaffolded: created }, null, 2));
} else {
console.log(`Scaffolded: ${result.path}`);
for (const path of created) {
console.log(` ${path}`);
}
}
} else if (values.json) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(result.path);
}
}
main().catch((err) => {
console.error("Error:", err.message);
process.exit(1);
});