ADR-003: Cross-Project ADR Aggregation
| Status | Accepted |
|---|---|
| Date | 2026-01-03 |
| Decision Makers | Christopher Joseph |
| Technical Story | Centralize architecture decisions from all portfolio projects |
| Depends On | ADR-001 (Enable Docs Plugin), ADR-002 (GitHub Projects Showcase) |
Summary for All Readers
This proposal creates an automated system to collect Architecture Decision Records (ADRs) from all GitHub projects listed in projects.json and aggregate them under a unified architecture section on amiable.dev. Each imported ADR is automatically tagged with its source repository and the project's technology keywords, making it easy to browse decisions by project or technology.
For recruiters: See consistent decision-making patterns across projects. For engineers: Discover architectural patterns and reasoning across the entire portfolio.
Context and Problem Statement
Architecture Decision Records (ADRs) document significant technical decisions with context, options considered, and rationale. Currently:
- ADRs are scattered across individual project repositories
- No unified view of architectural thinking across the portfolio
- Visitors must navigate to each repo separately to understand decisions
- Cross-project patterns and consistency are not visible
The GitHub Projects Showcase (ADR-002) already fetches project metadata at build time. Extending this pattern to fetch ADRs would provide a comprehensive architecture knowledge base.
Decision Drivers
- Centralization: Single location to browse all architectural decisions
- Discoverability: Find ADRs by project, technology, or decision type
- Freshness: ADRs update automatically when projects change
- Attribution: Clear indication of which project each ADR belongs to
- Resilience: Build succeeds even if some repos are inaccessible
- Minimal overhead: Leverage existing
projects.jsonconfiguration - Determinism: Reproducible builds via commit SHA pinning
Considered Options
Option 1: Build-time ADR Fetching Script (Recommended)
Extend the prebuild process to:
- For each project in
projects.json, discover ADR files using Git Trees API - Fetch ADR markdown files via raw.githubusercontent.com
- Transform content: rewrite relative links, inject front matter with namespaced slugs
- Write to
docs/adrs/projects/{repo-name}/ - Generate sidebar configuration automatically
Pros:
- Consistent with ADR-002 pattern (build-time data fetching)
- ADRs indexed by Docusaurus docs plugin with full-text search
- Automatic sidebar organization
- Graceful fallback if repos are inaccessible
- Full control over transformation and error handling
Cons:
- Increases build time (mitigated by caching and parallel fetches)
- Content duplicated (source of truth remains in project repos)
- Need to handle ADR format variations across projects
Option 2: Docusaurus Plugin with Remote Content
Use a plugin like docusaurus-plugin-remote-content to fetch ADRs.
Pros:
- Plugin ecosystem support
- Declarative configuration
Cons:
- Less control over front matter injection and slug namespacing
- Additional dependency to maintain
- May not handle multi-repo aggregation with custom tagging well
- Harder to implement asset rewriting
Option 3: Git Submodules
Include project repos as git submodules, referencing their ADR directories.
Pros:
- Direct file access, no API needed
- True single source of truth
Cons:
- Complex git workflow ("dependency hell")
- Submodule update management burden
- All repos must be public or accessible
- Poor CI/CD support (Vercel/Netlify submodule issues)
Option 4: Manual Curation
Copy ADRs manually when significant decisions are made.
Pros:
- Full editorial control
- No automation complexity
Cons:
- High maintenance burden
- ADRs quickly become stale
- Inconsistent coverage
Decision Outcome
Chosen option: Option 1 - Build-time ADR Fetching Script
This extends the proven pattern from ADR-002 while providing automatic tagging and organization. The script-based approach offers full control over transformation, caching, and error handling.
Technical Design
Directory Structure
docs/
├── adrs/
│ ├── ADR-001-enable-docs-plugin.md # Local ADRs
│ ├── ADR-002-github-projects-showcase.md
│ ├── ADR-003-cross-project-adr-aggregation.md
│ └── projects/ # Aggregated ADRs
│ ├── _category_.json # Sidebar config
│ ├── stentorosaur/
│ │ ├── _category_.json
│ │ ├── ADR-001-issue-based-monitoring.md
│ │ └── ADR-002-notification-system.md
│ ├── llm-council/
│ │ ├── _category_.json
│ │ ├── ADR-001-multi-model-deliberation.md
│ │ └── ADR-002-mcp-server-integration.md
│ └── conductor/
│ └── ADR-001-multi-protocol-input.md
GitHub API Strategy
Authentication (Required)
# CI environment must provide GITHUB_TOKEN
GITHUB_TOKEN=ghp_xxxx npm run build
- Unauthenticated: 60 requests/hour (insufficient)
- Authenticated: 5,000 requests/hour (required for production)
- Use least-privilege token with
public_reposcope only
Efficient Discovery via Git Trees API
Instead of checking multiple paths per repo, use a single Trees API call:
// Single call returns entire file tree
const { data } = await octokit.git.getTree({
owner, repo, tree_sha: 'HEAD', recursive: true
});
// Filter locally for ADR files
const adrFiles = data.tree.filter(file =>
file.type === 'blob' &&
file.path.match(/^(docs\/adrs?|adr|docs\/architecture\/decisions)\/.*\.md$/i)
);
Content Fetching via Raw URLs
After discovery, fetch content via raw.githubusercontent.com (no rate limit):
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${commitSha}/${filePath}`;
const content = await fetch(rawUrl).then(r => r.text());
Front Matter Injection and Merge Strategy
Each fetched ADR receives injected front matter with explicit merge rules:
---
# INJECTED BY AGGREGATOR (always wins)
slug: /docs/adrs/projects/stentorosaur/adr-001 # Namespaced to prevent collisions
source_repo: amiable-dev/stentorosaur
source_url: https://github.com/amiable-dev/stentorosaur/blob/main/docs/adrs/ADR-001.md
source_commit: a1b2c3d4e5f6
fetched_at: 2026-01-03T12:00:00Z
tags: # Aggregator adds, source extends
- stentorosaur
- typescript
- docusaurus
- github-api
sidebar_label: "ADR-001: Issue-Based Monitoring"
# PRESERVED FROM SOURCE (source wins)
title: "ADR-001: Issue-Based Status Monitoring" # If present in source
date: 2025-06-15 # If present in source
authors: [chris] # If present in source
---
Merge Rules:
| Field | Winner | Rationale |
|---|---|---|
slug | Aggregator | Prevents route collisions |
source_* fields | Aggregator | Attribution metadata |
tags | Merged | Aggregator adds repo/skills, source adds custom |
title, date, authors | Source | Preserve original metadata |
sidebar_label | Aggregator | Consistent navigation |
Asset and Link Handling
ADRs often contain relative links that break when aggregated. Strategy:
Images and Diagrams
Rewrite relative image paths to absolute GitHub URLs:
// Before: 
// After: 
content = content.replace(
/!\[([^\]]*)\]\(\.?\/?([^)]+)\)/g,
(match, alt, path) => {
if (path.startsWith('http')) return match; // Already absolute
const absolutePath = resolvePath(adrDir, path);
return ``;
}
);
Internal ADR Links
Rewrite links to other ADRs in the same repo:
// Before: [See ADR-002](./ADR-002-notifications.md)
// After: [See ADR-002](/docs/adrs/projects/stentorosaur/adr-002-notifications)
External Links
Leave unchanged (already absolute).
Caching Strategy
Cache Location
.cache/
└── adrs/
├── manifest.json # Tracks commit SHAs per repo
└── projects/
├── stentorosaur/
│ └── *.md # Cached transformed content
└── llm-council/
└── *.md
Cache Invalidation
// manifest.json
{
"stentorosaur": {
"commitSha": "a1b2c3d4",
"fetchedAt": "2026-01-03T12:00:00Z",
"files": ["ADR-001.md", "ADR-002.md"]
}
}
// On build: compare current HEAD SHA vs cached SHA
// If unchanged, skip fetching (use cached files)
// If changed, fetch and update cache
Conditional Requests
Use ETags for efficient API usage:
const response = await octokit.git.getTree({
owner, repo, tree_sha: 'HEAD',
headers: { 'If-None-Match': cachedEtag }
});
if (response.status === 304) {
// Use cached content
}
Cache Persistence
.cache/is git-ignored- CI caches
.cache/between builds for speed - Fallback: if API fails and cache exists, use cached content with
stale: truewarning
Deleted Content Cleanup
To prevent stale ADRs from persisting:
async function syncProjectADRs(project) {
const targetDir = `docs/adrs/projects/${project.name}`;
// 1. Fetch current ADR list from source
const sourceFiles = await discoverADRs(project);
// 2. List existing local files
const localFiles = await fs.readdir(targetDir);
// 3. Delete files that no longer exist in source
for (const localFile of localFiles) {
if (!sourceFiles.includes(localFile) && localFile !== '_category_.json') {
await fs.unlink(path.join(targetDir, localFile));
console.log(`Removed stale ADR: ${project.name}/${localFile}`);
}
}
// 4. Fetch and write current ADRs
for (const sourceFile of sourceFiles) {
await fetchAndTransform(project, sourceFile);
}
}
Security and Sanitization
Content Trust Model
- Only fetch from repos listed in
projects.json(curated allowlist) - All repos are owned by the portfolio owner (trusted source)
MDX Handling
Imported ADRs are treated as Markdown only (not MDX):
// Rename .mdx to .md if encountered
// Strip any import/export statements
content = content.replace(/^(import|export)\s+.*$/gm, '');
HTML Sanitization
Docusaurus sanitizes HTML by default, but for defense in depth:
// Warn on suspicious patterns
if (content.match(/<script|javascript:|on\w+=/i)) {
console.warn(`Suspicious content in ${filePath}, skipping`);
return null;
}
Token Security
GITHUB_TOKENused only at build time, never bundled- CI secrets configured via GitHub Actions / hosting provider
- Token has minimal
public_reporead scope
Build Process Integration
npm run build
└── prebuild (package.json script)
├── scripts/fetch-projects.js # Existing (ADR-002)
└── scripts/fetch-adrs.js # New
├── Read projects.json
├── Check cache manifest
├── For each project (parallel with p-limit):
│ ├── Compare HEAD SHA vs cached SHA
│ ├── Skip if unchanged (use cache)
│ ├── Discover ADRs via Trees API
│ ├── Fetch content via raw URLs
│ ├── Transform (front matter, links)
│ ├── Clean stale local files
│ └── Write to docs/adrs/projects/{repo}/
├── Generate _category_.json files
└── Update cache manifest
Local Development
ADR fetching is not run on npm start to avoid rate limits during hot reload:
{
"scripts": {
"prebuild": "node scripts/fetch-adrs.js && node scripts/fetch-projects.js",
"start": "docusaurus start",
"fetch-adrs": "node scripts/fetch-adrs.js"
}
}
Developers run npm run fetch-adrs manually when needed.
ADR Discovery Heuristics
Projects may store ADRs in different locations. The Trees API discovers all files, then we filter:
| Priority | Path Pattern | Description |
|---|---|---|
| 1 | docs/adrs/*.md | Standard location |
| 2 | docs/adr/*.md | Alternative singular |
| 3 | adr/*.md | Root-level ADRs |
| 4 | docs/architecture/decisions/*.md | Verbose path |
Naming Normalization
- Case-insensitive matching (
ADR,adr,Adr) - Support common patterns:
ADR-001.md,0001-decision.md,adr-001-title.md
Error Handling
Key principle: The build should never fail due to GitHub API issues.
Sidebar Configuration
Auto-generate _category_.json files:
{
"label": "Project ADRs",
"position": 10,
"collapsible": true,
"collapsed": false,
"link": {
"type": "generated-index",
"title": "Architecture Decisions by Project",
"description": "Browse architecture decision records from all portfolio projects."
}
}
Per-project category:
{
"label": "Stentorosaur",
"position": 1,
"link": {
"type": "generated-index",
"title": "Stentorosaur ADRs",
"description": "Architecture decisions for the Stentorosaur status monitoring plugin."
}
}
Tagging Strategy
Automatic Tags
Each ADR receives tags from multiple sources:
| Tag Source | Example Tags | Normalization |
|---|---|---|
| Repository name | stentorosaur | lowercase |
| Project skills (from projects.json) | typescript, docusaurus | lowercase, hyphenate spaces |
| Source ADR tags (if present) | security, api | preserved |
Tag Normalization
function normalizeTag(tag) {
return tag
.toLowerCase()
.replace(/\s+/g, '-') // "GitHub API" → "github-api"
.replace(/[^a-z0-9-]/g, ''); // Remove special chars
}
UI Enhancements
ADR Index Page
Auto-generated at /docs/adrs/projects/:
# Project Architecture Decisions
Browse architecture decision records from across the portfolio.
## By Project
- [Stentorosaur](/docs/adrs/projects/stentorosaur/) (3 ADRs)
- [LLM Council](/docs/adrs/projects/llm-council/) (5 ADRs)
- [Conductor](/docs/adrs/projects/conductor/) (2 ADRs)
## By Technology
Use tags to filter: [typescript] [python] [mcp] [rust]
Source Attribution Banner
Each aggregated ADR displays a banner via Docusaurus admonition:
:::info[Source Repository]
This ADR is from the **Stentorosaur** project.
[View original](https://github.com/amiable-dev/stentorosaur/blob/a1b2c3d/docs/adrs/ADR-001.md) · Last synced: Jan 3, 2026
:::
Implementation Plan
Phase 1: Script Development
- Create
scripts/fetch-adrs.js - Implement GitHub Trees API integration with authentication
- Implement raw content fetching with link rewriting
- Implement front matter parsing, merging, and injection
- Add caching layer with manifest and ETag support
- Add stale content cleanup
- Update prebuild:
"prebuild": "node scripts/fetch-projects.js && node scripts/fetch-adrs.js"
Phase 2: Docusaurus Integration
- Configure sidebar for
docs/adrs/projects/ - Create index page with project groupings
- Implement source attribution admonition component
- Test tag-based filtering and search
Phase 3: Testing
- Unit tests for front matter injection and link rewriting
- Integration tests for multi-repo fetching
- Verify graceful degradation (missing ADRs, API failures, no token)
- Test cache invalidation and cleanup
- Verify no slug collisions
Phase 4: Documentation
- Update CLAUDE.md with ADR aggregation section
- Document ADR naming conventions for projects
- Add troubleshooting guide for common issues
Security Considerations
| Concern | Mitigation |
|---|---|
| Fetching from untrusted repos | Only fetch from repos in projects.json (curated allowlist) |
| Malicious content in ADRs | Treat as Markdown only (no MDX); Docusaurus sanitizes HTML |
| Token exposure | Build-time only; CI secrets; never in client bundle; minimal scope |
| Rate limits | Authenticated requests (5k/hr); Trees API; aggressive caching |
| Private repos | Excluded; only public repos in portfolio |
Consequences
Positive
- Unified view of architectural thinking across portfolio
- Discoverability via tags and full-text search
- Demonstrates consistent decision-making practices
- ADRs automatically stay current with source repos
- Cross-project pattern recognition
- Reproducible builds via commit SHA tracking
Negative
- Increased build time (~10-30 seconds for initial fetch, cached thereafter)
- Duplicated content (source of truth in project repos)
- Need to handle format variations across projects
- More complex prebuild process
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Broken relative images/links | High | High | Rewrite to absolute GitHub raw URLs |
| GitHub API rate limiting | Medium | High | GITHUB_TOKEN auth; Trees API; ETag caching |
| Slug/route collisions | Medium | Medium | Namespace slugs by project: /adrs/project/adr-001 |
| Projects lack ADRs | High | Low | Skip gracefully; document ADR adoption |
| Inconsistent ADR formats | Medium | Medium | Flexible parser; normalize during transform |
| Build determinism issues | Medium | Low | Pin to commit SHAs; record in manifest |
| Stale aggregated ADRs | Medium | Low | Show sync timestamp; cache invalidation via SHA |
| Large ADR counts slow build | Low | Medium | Parallel fetches with p-limit; caching |
| Malicious content injection | Low | High | Curated allowlist; Markdown-only; HTML sanitization |
Success Metrics
- 80%+ of portfolio projects have at least one ADR aggregated
- ADR section becomes top-10 visited docs section
- Cross-project architectural patterns documented
- Build time increase <30 seconds (cached builds <5 seconds)
- Zero build failures due to API issues
Implemented Enhancements (v2)
The following enhancements were reviewed and approved by the LLM Council (2026-01-03) and have been fully implemented.
Enhancement 1: Template/Boilerplate Exclusion
Problem: ADR-000 template files are aggregated but aren't actual architectural decisions.
Solution: Configurable exclusion patterns with sensible defaults.
// Default exclusion config in fetch-adrs.js
const DEFAULT_EXCLUSIONS = {
patterns: [
/^ADR-000/i, // Standard template number
/template/i, // Files with "template" in name
/^0000-/, // Alternative template numbering
],
exact: [
'TEMPLATE.md',
'template.md',
]
};
Per-project overrides in projects.json:
{
"repo": "amiable-dev/llm-council",
"adrConfig": {
"excludePatterns": ["/^ADR-000/i"],
"includeTemplates": true
}
}
Implementation notes:
- Log excluded files during fetch for debuggability
- Add regex escaping utility for safety in overrides
Enhancement 2: ADR Summary Dashboard
Problem: No unified view of ADR status across projects.
Solution: Generate a summary page at /docs/adrs/projects/ as the category index.
# Project ADR Summary
| Project | Total ADRs | Status Breakdown | Last Updated |
|---------|------------|------------------|--------------|
| LLM Council | 34 | 🟢 28 Accepted, 🟡 4 Proposed, 🔴 2 Deprecated | Jan 3, 2026 |
| Stentorosaur | 2 | 🟢 2 Accepted | Jan 3, 2026 |
## Status Distribution
- **Accepted**: 35 (80%)
- **Proposed**: 6 (14%)
- **Deprecated**: 2 (5%)
## Trends
+3 ADRs this month 📈
Status normalization (preserve original in front matter):
| Original Status | Normalized |
|---|---|
| Proposed, Draft, Review | Proposed |
| Accepted, Approved | Accepted |
| Deprecated, Superseded, Amends | Deprecated |
| Other | Unknown |
Implementation:
- Parse ADR front matter for
statusfield during aggregation - Generate summary statistics in
fetch-adrs.js - Write to
docs/adrs/projects/index.mdas category index - Use emoji badges (🟢🟡🔴) for quick scanning
Enhancement 3: Projects Page ↔ ADRs Cross-Linking
Problem: Projects page doesn't link to architecture decisions.
Solution: Bidirectional linking via static data enrichment.
A. Projects → ADRs (in ProjectCard.tsx):
// Uses slugified title to match Docusaurus category URL generation
{project.adrCount > 0 && (
<a href={`/docs/category/${slugifyTitle(project.title)}`}>
{project.adrCount} ADRs
</a>
)}
B. ADRs → Projects (in attribution banner):
:::info[Source Repository]
This ADR is from **[LLM Council](/projects#llm-council)**.
[View original](https://github.com/...) · [All project ADRs](/docs/adrs/projects/llm-council/)
:::
C. Data flow:
fetch-adrs.jswrites ADR counts tosrc/data/adr-summary.jsonfetch-projects.jsreadsadr-summary.jsonto enrich withadrCount- Zero runtime cost, consistent with static generation
Implementation Status
All v2 enhancements implemented using TDD methodology (165 tests passing). Implementation followed council recommendation: 1 → 3 → 2 (exclusions first to clean data, then linking, then summary).
Future Enhancements
- ADR search: Full-text search across all aggregated ADRs
- ADR timeline: Chronological view of decisions across projects
- Decision matrix: Compare similar decisions across projects
- Webhook triggers: Rebuild when source repos push ADR changes
- Private repo support: Optional authenticated fetching for private ADRs
- ADR velocity metrics: Track avg days Proposed → Accepted per project
- API endpoint: Expose
/api/adrs-summary.jsonfor embeds/tools
Related Decisions
- ADR-001: Enable Docs Plugin (provides docs infrastructure)
- ADR-002: GitHub Projects Showcase (establishes build-time fetching pattern, shares
projects.json)