Skip to main content

ADR-003: Cross-Project ADR Aggregation

StatusAccepted
Date2026-01-03
Decision MakersChristopher Joseph
Technical StoryCentralize architecture decisions from all portfolio projects
Depends OnADR-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.json configuration
  • Determinism: Reproducible builds via commit SHA pinning

Considered Options

Extend the prebuild process to:

  1. For each project in projects.json, discover ADR files using Git Trees API
  2. Fetch ADR markdown files via raw.githubusercontent.com
  3. Transform content: rewrite relative links, inject front matter with namespaced slugs
  4. Write to docs/adrs/projects/{repo-name}/
  5. 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_repo scope 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:

FieldWinnerRationale
slugAggregatorPrevents route collisions
source_* fieldsAggregatorAttribution metadata
tagsMergedAggregator adds repo/skills, source adds custom
title, date, authorsSourcePreserve original metadata
sidebar_labelAggregatorConsistent navigation

ADRs often contain relative links that break when aggregated. Strategy:

Images and Diagrams

Rewrite relative image paths to absolute GitHub URLs:

// Before: ![Diagram](./assets/architecture.png)
// After: ![Diagram](https://raw.githubusercontent.com/owner/repo/commit/docs/adrs/assets/architecture.png)

content = content.replace(
/!\[([^\]]*)\]\(\.?\/?([^)]+)\)/g,
(match, alt, path) => {
if (path.startsWith('http')) return match; // Already absolute
const absolutePath = resolvePath(adrDir, path);
return `![${alt}](https://raw.githubusercontent.com/${owner}/${repo}/${commitSha}/${absolutePath})`;
}
);

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: true warning

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_TOKEN used only at build time, never bundled
  • CI secrets configured via GitHub Actions / hosting provider
  • Token has minimal public_repo read 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:

PriorityPath PatternDescription
1docs/adrs/*.mdStandard location
2docs/adr/*.mdAlternative singular
3adr/*.mdRoot-level ADRs
4docs/architecture/decisions/*.mdVerbose 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.

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 SourceExample TagsNormalization
Repository namestentorosaurlowercase
Project skills (from projects.json)typescript, docusauruslowercase, hyphenate spaces
Source ADR tags (if present)security, apipreserved

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

  1. Create scripts/fetch-adrs.js
  2. Implement GitHub Trees API integration with authentication
  3. Implement raw content fetching with link rewriting
  4. Implement front matter parsing, merging, and injection
  5. Add caching layer with manifest and ETag support
  6. Add stale content cleanup
  7. Update prebuild: "prebuild": "node scripts/fetch-projects.js && node scripts/fetch-adrs.js"

Phase 2: Docusaurus Integration

  1. Configure sidebar for docs/adrs/projects/
  2. Create index page with project groupings
  3. Implement source attribution admonition component
  4. Test tag-based filtering and search

Phase 3: Testing

  1. Unit tests for front matter injection and link rewriting
  2. Integration tests for multi-repo fetching
  3. Verify graceful degradation (missing ADRs, API failures, no token)
  4. Test cache invalidation and cleanup
  5. Verify no slug collisions

Phase 4: Documentation

  1. Update CLAUDE.md with ADR aggregation section
  2. Document ADR naming conventions for projects
  3. Add troubleshooting guide for common issues

Security Considerations

ConcernMitigation
Fetching from untrusted reposOnly fetch from repos in projects.json (curated allowlist)
Malicious content in ADRsTreat as Markdown only (no MDX); Docusaurus sanitizes HTML
Token exposureBuild-time only; CI secrets; never in client bundle; minimal scope
Rate limitsAuthenticated requests (5k/hr); Trees API; aggressive caching
Private reposExcluded; 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

RiskLikelihoodImpactMitigation
Broken relative images/linksHighHighRewrite to absolute GitHub raw URLs
GitHub API rate limitingMediumHighGITHUB_TOKEN auth; Trees API; ETag caching
Slug/route collisionsMediumMediumNamespace slugs by project: /adrs/project/adr-001
Projects lack ADRsHighLowSkip gracefully; document ADR adoption
Inconsistent ADR formatsMediumMediumFlexible parser; normalize during transform
Build determinism issuesMediumLowPin to commit SHAs; record in manifest
Stale aggregated ADRsMediumLowShow sync timestamp; cache invalidation via SHA
Large ADR counts slow buildLowMediumParallel fetches with p-limit; caching
Malicious content injectionLowHighCurated 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 StatusNormalized
Proposed, Draft, ReviewProposed
Accepted, ApprovedAccepted
Deprecated, Superseded, AmendsDeprecated
OtherUnknown

Implementation:

  1. Parse ADR front matter for status field during aggregation
  2. Generate summary statistics in fetch-adrs.js
  3. Write to docs/adrs/projects/index.md as category index
  4. 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:

  1. fetch-adrs.js writes ADR counts to src/data/adr-summary.json
  2. fetch-projects.js reads adr-summary.json to enrich with adrCount
  3. 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

  1. ADR search: Full-text search across all aggregated ADRs
  2. ADR timeline: Chronological view of decisions across projects
  3. Decision matrix: Compare similar decisions across projects
  4. Webhook triggers: Rebuild when source repos push ADR changes
  5. Private repo support: Optional authenticated fetching for private ADRs
  6. ADR velocity metrics: Track avg days Proposed → Accepted per project
  7. API endpoint: Expose /api/adrs-summary.json for embeds/tools
  • ADR-001: Enable Docs Plugin (provides docs infrastructure)
  • ADR-002: GitHub Projects Showcase (establishes build-time fetching pattern, shares projects.json)