Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,20 @@ Each directory has `_template.md` and `README.md`. Key structures:
- `refactor:` - Reorganizing without changing meaning
- `chore:` - Maintenance tasks

## Changelog Requirements

**Always update CHANGELOG.md** when adding new content or making significant changes:

1. Add entry to `[Unreleased]` section
2. Use semantic prefix: `feat(pattern):`, `feat(vendor):`, `fix:`, `docs:`, etc.
3. Link to the file: `[Name](path/to/file.md)`
4. Link to PR: `([#123](https://github.com/ethereum/iptf-map/pull/123))`

Example:
```markdown
- feat(pattern): [New pattern name](patterns/pattern-slug.md) ([#123](https://github.com/ethereum/iptf-map/pull/123))
```

## Content Organization

1. **Patterns** = Reusable building blocks (single architectural solution)
Expand Down
86 changes: 81 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,43 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4

- name: Check vendor neutrality
- name: Check content neutrality
run: |
echo "Checking vendor documentation for neutrality..."
# Check for marketing language patterns
if grep -r -i -E "(best|leading|superior|unmatched|revolutionary)" vendors/*.md 2>/dev/null; then
echo "Checking documentation for marketing language..."

# Extended marketing language patterns
MARKETING_WORDS="best|leading|superior|unmatched|revolutionary|cutting-edge|world-class|industry-leading|state-of-the-art|groundbreaking|innovative|seamless|robust|scalable|enterprise-grade|next-generation|unique|only|first|fastest|most secure|unparalleled|comprehensive|end-to-end|one-stop|game-changing|disruptive|bleeding-edge|turnkey|holistic|best-in-class|market-leading|unrivaled|premier|top-tier|ultimate|unprecedented"

FOUND_ISSUES=false

# Check vendors (stricter)
echo "::group::Vendor documentation"
if grep -r -i -E "($MARKETING_WORDS)" vendors/*.md 2>/dev/null | grep -v -f .marketing-exceptions.txt 2>/dev/null || grep -r -i -E "($MARKETING_WORDS)" vendors/*.md 2>/dev/null; then
echo "::warning::Found potential marketing language in vendor documentation"
FOUND_ISSUES=true
fi
echo "::endgroup::"

# Check patterns (warning)
echo "::group::Pattern documentation"
if grep -r -i -E "($MARKETING_WORDS)" patterns/*.md 2>/dev/null | grep -v "_template.md" | grep -v -f .marketing-exceptions.txt 2>/dev/null || grep -r -i -E "($MARKETING_WORDS)" patterns/*.md 2>/dev/null | grep -v "_template.md"; then
echo "::warning::Found potential marketing language in pattern documentation"
FOUND_ISSUES=true
fi
echo "::endgroup::"

# Check approaches (warning)
echo "::group::Approach documentation"
if grep -r -i -E "($MARKETING_WORDS)" approaches/*.md 2>/dev/null | grep -v "_template.md" | grep -v -f .marketing-exceptions.txt 2>/dev/null || grep -r -i -E "($MARKETING_WORDS)" approaches/*.md 2>/dev/null | grep -v "_template.md"; then
echo "::warning::Found potential marketing language in approach documentation"
FOUND_ISSUES=true
fi
echo "::endgroup::"

if [ "$FOUND_ISSUES" = true ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **Marketing Language Check**" >> $GITHUB_STEP_SUMMARY
echo "Found potential marketing language. Review and use neutral, factual terms." >> $GITHUB_STEP_SUMMARY
fi

- name: Verify vendor cross-references
Expand All @@ -87,4 +118,49 @@ jobs:
exit 1
fi
fi
done
done

validate-changelog:
runs-on: ubuntu-latest
name: Validate Changelog
if: github.event_name == 'pull_request'

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check changelog update
run: |
# Get list of changed files in PR
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)

# Check if content directories were modified
CONTENT_CHANGED=false
for pattern in "patterns/" "vendors/" "approaches/" "use-cases/" "jurisdictions/" "domains/"; do
if echo "$CHANGED_FILES" | grep -q "^$pattern"; then
CONTENT_CHANGED=true
break
fi
done

# Check if CHANGELOG.md was updated
CHANGELOG_UPDATED=false
if echo "$CHANGED_FILES" | grep -q "^CHANGELOG.md"; then
CHANGELOG_UPDATED=true
fi

# Warn if content changed but changelog wasn't updated
if [ "$CONTENT_CHANGED" = true ] && [ "$CHANGELOG_UPDATED" = false ]; then
echo "::warning::Content files were modified but CHANGELOG.md was not updated."
echo "::warning::Please add an entry to the [Unreleased] section in CHANGELOG.md"
echo "" >> $GITHUB_STEP_SUMMARY
echo "⚠️ **Changelog Reminder**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Content files were modified. Please update CHANGELOG.md:" >> $GITHUB_STEP_SUMMARY
echo "1. Add entry to \`[Unreleased]\` section" >> $GITHUB_STEP_SUMMARY
echo "2. Use format: \`- feat(type): [Name](path) ([#PR](url))\`" >> $GITHUB_STEP_SUMMARY
elif [ "$CHANGELOG_UPDATED" = true ]; then
echo "✅ CHANGELOG.md was updated" >> $GITHUB_STEP_SUMMARY
fi
20 changes: 20 additions & 0 deletions .marketing-exceptions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Marketing language exceptions
# These phrases are allowed because they are technical terms or proper names

# Glossary terms
Best Execution
best execution

# Technical terms that happen to contain marketing words
unique identifier
unique ID
first-come-first-served
end-to-end encryption
end-to-end encrypted

# Describing limitations (not promotional)
only works with
only supports
only available
first step
first phase
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ All notable changes to the IPTF Map are documented here.
- feat(pattern): [Threshold encrypted mempool](patterns/pattern-threshold-encrypted-mempool.md) ([#45](https://github.com/ethereum/iptf-map/pull/45))
- feat(vendor): [Fhenix](vendors/fhenix.md) - FHE privacy ([#32](https://github.com/ethereum/iptf-map/pull/32))
- feat(ci): Pattern validation workflow ([#40](https://github.com/ethereum/iptf-map/pull/40))
- feat(approach): [Private bond PoC](approaches/approach-private-bonds.md) updates ([#47](https://github.com/ethereum/iptf-map/pull/47))
- docs: [Q1 2026 PRD](PRD-IPTF-PUBLIC-Q1-2026.md) with sprint planning ([#39](https://github.com/ethereum/iptf-map/pull/39))
- docs: [CHANGELOG.md](CHANGELOG.md) and weekly summary script ([#49](https://github.com/ethereum/iptf-map/pull/49))
- docs(prd): Sprint 6 for client-side proving and compliance
- docs(prd): Source citations for accuracy verification findings
- docs(prd): Sprint 1 completion and accuracy findings
- docs(prd): GitHub issue traceability and missing deliverables
- chore: Claude Code project configuration ([#36](https://github.com/ethereum/iptf-map/pull/36))

### Fixed
- fix(pattern): Required frontmatter fields across all patterns ([#42](https://github.com/ethereum/iptf-map/pull/42))
Expand Down
38 changes: 38 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# IPTF Map - Local Validation Commands
# Run these before pushing to catch issues early

.PHONY: help install validate lint check marketing-check

help:
@echo "IPTF Map Validation Commands"
@echo ""
@echo " make install - Install dependencies"
@echo " make validate - Run pattern validation"
@echo " make lint - Run markdown linting"
@echo " make check - Run all checks (validate + lint)"
@echo " make marketing-check - Check for marketing language"
@echo ""

install:
cd scripts && npm install

validate:
node scripts/validate-patterns.js

lint:
@echo "Running markdownlint..."
@npx markdownlint-cli2 "patterns/*.md" "approaches/*.md" "use-cases/*.md" "vendors/*.md" 2>/dev/null || echo "Install markdownlint-cli2: npm install -g markdownlint-cli2"

marketing-check:
@echo "Checking for marketing language..."
@MARKETING_WORDS="best|leading|superior|unmatched|revolutionary|cutting-edge|world-class|industry-leading|state-of-the-art|groundbreaking|innovative|seamless|robust|scalable|enterprise-grade|next-generation|unique|only|first|fastest|most secure|unparalleled|comprehensive|end-to-end|one-stop|game-changing|disruptive|bleeding-edge|turnkey|holistic|best-in-class|market-leading|unrivaled|premier|top-tier|ultimate|unprecedented"; \
if grep -r -i -E "($$MARKETING_WORDS)" vendors/*.md patterns/*.md approaches/*.md 2>/dev/null | grep -v "_template.md"; then \
echo ""; \
echo "⚠️ Found potential marketing language. Review and use neutral terms."; \
else \
echo "✅ No marketing language detected"; \
fi

check: validate lint marketing-check
@echo ""
@echo "✅ All checks complete"
78 changes: 78 additions & 0 deletions scripts/validate-patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ const yaml = require('js-yaml');
// Configuration
const PATTERNS_DIR = path.join(__dirname, '..', 'patterns');
const VENDORS_DIR = path.join(__dirname, '..', 'vendors');
const APPROACHES_DIR = path.join(__dirname, '..', 'approaches');
const USE_CASES_DIR = path.join(__dirname, '..', 'use-cases');
const ROOT_DIR = path.join(__dirname, '..');
const IS_CI = process.env.CI_MODE === 'strict';

// Word count limits for conciseness
const WORD_LIMITS = {
patterns: { warn: 800, error: 1500 },
vendors: { warn: 1000, error: 1800 },
approaches: { warn: 2000, error: 3000 }
};

// Required frontmatter fields for patterns
const REQUIRED_PATTERN_FRONTMATTER = [
'title',
Expand Down Expand Up @@ -81,6 +91,59 @@ function parseMarkdown(filePath) {
return { frontmatter, content: markdownContent };
}

/**
* Extract and validate internal markdown links
*/
function validateInternalLinks(filePath, content) {
const linkErrors = [];
const dir = path.dirname(filePath);

// Match markdown links: [text](path)
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
let match;

while ((match = linkRegex.exec(content)) !== null) {
const linkPath = match[2];

// Skip external links, anchors, and special protocols
if (linkPath.startsWith('http://') ||
linkPath.startsWith('https://') ||
linkPath.startsWith('#') ||
linkPath.startsWith('mailto:')) {
continue;
}

// Handle relative .md links
if (linkPath.endsWith('.md') || linkPath.includes('.md#')) {
const mdPath = linkPath.split('#')[0]; // Remove anchor
const resolvedPath = path.resolve(dir, mdPath);

if (!fs.existsSync(resolvedPath)) {
linkErrors.push(`Broken internal link: ${linkPath}`);
}
}
}

return linkErrors;
}

/**
* Count words in content (excluding frontmatter and code blocks)
*/
function countWords(content) {
// Remove code blocks
let text = content.replace(/```[\s\S]*?```/g, '');
// Remove inline code
text = text.replace(/`[^`]+`/g, '');
// Remove links but keep text
text = text.replace(/\[([^\]]*)\]\([^)]+\)/g, '$1');
// Remove markdown formatting
text = text.replace(/[#*_\[\]]/g, ' ');
// Split and count
const words = text.split(/\s+/).filter(w => w.length > 0);
return words.length;
}

/**
* Validate pattern file naming convention
*/
Expand Down Expand Up @@ -165,6 +228,21 @@ function validatePattern(filePath) {
}
}

// Validate internal links (warning only - enable blocking after fixing existing issues)
const linkErrors = validateInternalLinks(filePath, content);
if (linkErrors.length > 0) {
fileWarnings.push(...linkErrors);
}

// Check word count for conciseness
const wordCount = countWords(content);
const limits = WORD_LIMITS.patterns;
if (wordCount > limits.error) {
fileErrors.push(`Content too long: ${wordCount} words (max ${limits.error})`);
} else if (wordCount > limits.warn) {
fileWarnings.push(`Content length: ${wordCount} words (recommended max ${limits.warn})`);
}

// Report errors and warnings
if (fileErrors.length > 0) {
errors.push(`\n❌ ${fileName}:\n ${fileErrors.join('\n ')}`);
Expand Down
Loading