diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0d3ea99..4cc5bc9 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20013cf..a5d809c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -87,4 +118,49 @@ jobs: exit 1 fi fi - done \ No newline at end of file + 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 \ No newline at end of file diff --git a/.marketing-exceptions.txt b/.marketing-exceptions.txt new file mode 100644 index 0000000..f16a8a7 --- /dev/null +++ b/.marketing-exceptions.txt @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b71ed..f96770a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4b0b0e3 --- /dev/null +++ b/Makefile @@ -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" diff --git a/scripts/validate-patterns.js b/scripts/validate-patterns.js index 17c6ee7..ca2ba47 100644 --- a/scripts/validate-patterns.js +++ b/scripts/validate-patterns.js @@ -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', @@ -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 */ @@ -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 ')}`);