Motivation¶
The ai_engineering_book repository contains several validation scripts in tools/scripts/ that share common patterns: YAML frontmatter parsing, configuration loading, validation error reporting. Each script duplicates parsing logic, and the validation functionality cannot be reused in other repositories.
Solution: Extract common validation logic into a reusable Python package (vadocs) that can be installed via uv add.
Goal¶
Create a minimal viable package that proves:
Installability as a uv dependency from GitHub
YAML frontmatter validation works correctly
ADR-specific validation (fields, status, tags, sections)
Configuration loading from YAML files
Implementation Summary¶
Core models:
Document,ValidationError,SyncField,SyncResultValidators:
FrontmatterValidator,AdrValidator,AdrTermValidatorFixers:
AdrFixer,SyncFixerConfig:
load_config()for YAML filesTests: 79 passing
What’s NOT in v0.1.0¶
CLI interface (library-only)
pyproject.toml
[tool.vadocs]config loadingProject scaffolding (
vadocs init)Index sync validation
Further Steps¶
v0.2.0: CLI + pyproject.toml config
v0.3.0: Index sync validation
v0.4.0: Additional validators (broken links, jupytext)
v1.0.0: PyPI release, stable API
PoC Validation Checklist¶
Execute each step manually. Mark [x] when passed.
1. Installation¶
cd /tmp/test-vadocs
ls -a. .. adr adr_index.md packages post-mortems README.md what_is_an_adr.md
uv init
env -u VIRTUAL_ENV uv add "vadocs @ git+https://github.com/lefthand67/vadocs.git"Initialized project `test-vadocs`
Using CPython 3.12.11 interpreter at: /usr/local/bin/python3.12
Creating virtual environment at: .venv
Resolved 11 packages in 703ms
░░░░░░░░░░░░░░░░░░░░ [0/9] Installing wheels... warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
If the cache and target directories are on different filesystems, hardlinking may not be supported.
If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
Installed 9 packages in 12ms
+ coverage==7.13.3
+ iniconfig==2.3.0
+ packaging==26.0
+ pluggy==1.6.0
+ pygments==2.19.2
+ pytest==9.0.2
+ pytest-cov==7.0.0
+ pyyaml==6.0.3
+ vadocs==0.1.0 (from git+https://github.com/lefthand67/vadocs.git@f742e6ccabb27e6fc55c60b26166014ba3ac0498)
env -u VIRTUAL_ENV uv treeResolved 11 packages in 0.64ms
test-vadocs v0.1.0
└── vadocs v0.1.0
├── pytest v9.0.2
│ ├── iniconfig v2.3.0
│ ├── packaging v26.0
│ ├── pluggy v1.6.0
│ └── pygments v2.19.2
├── pytest-cov v7.0.0
│ ├── coverage v7.13.3
│ ├── pluggy v1.6.0
│ └── pytest v9.0.2 (*)
└── pyyaml v6.0.3
(*) Package tree already displayed
cat pyproject.toml[project]
name = "test-vadocs"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"vadocs",
]
[tool.uv.sources]
vadocs = { git = "https://github.com/lefthand67/vadocs.git" }
ls -a. adr_index.md main.py pyproject.toml uv.lock
.. .git packages .python-version .venv
adr .gitignore post-mortems README.md what_is_an_adr.md
PASS: Command completes without errors
2. Import verification¶
env -u VIRTUAL_ENV uv run python -c "from vadocs import AdrValidator, Document, load_config, parse_frontmatter; print('OK')"OK
PASS: Prints
OK
3. Config loading¶
env -u VIRTUAL_ENV uv run python -c "
from pathlib import Path
from vadocs import load_config
config = load_config(Path('adr/adr_config.yaml'))
print('statuses:', config.get('statuses'))
print('required_fields:', config.get('required_fields'))
"statuses: ['proposed', 'accepted', 'rejected', 'superseded', 'deprecated']
required_fields: ['id', 'title', 'date', 'status', 'tags']
PASS: Prints statuses list (proposed, accepted, etc.)
PASS: Prints required_fields list (id, title, date, status, tags)
4. Valid ADR produces no errors¶
Pick a known valid ADR file and run:
env -u VIRTUAL_ENV uv run python -c "
from pathlib import Path
from vadocs import AdrValidator, Document, load_config, parse_frontmatter
config = load_config(Path('adr/adr_config.yaml'))
adr_path = Path('adr/adr_26017_adr_format_validation_workflow.md') # <-- change to your ADR
content = adr_path.read_text()
doc = Document(path=adr_path, content=content, frontmatter=parse_frontmatter(content), doc_type='adr')
errors = AdrValidator().validate(doc, config)
print(f'Errors: {len(errors)}')
for e in errors: print(f' [{e.error_type}] {e.message}')
"Errors: 0
cat > /tmp/test_adr.md <<EOF
---
id: 99999
title: Test ADR
status: accepted
---
# ADR-99999: Test ADR
EOFRun validation:
env -u VIRTUAL_ENV uv run python -c "
from pathlib import Path
from vadocs import AdrValidator, Document, load_config, parse_frontmatter
config = load_config(Path('adr/adr_config.yaml'))
content = Path('/tmp/test_adr.md').read_text()
doc = Document(path=Path('/tmp/test_adr.md'), content=content, frontmatter=parse_frontmatter(content), doc_type='adr')
errors = AdrValidator().validate(doc, config)
print(f'Errors: {len(errors)}')
for e in errors: print(f' [{e.error_type}] {e.message}')
"Errors: 10
[missing_field] ADR 99999 missing required field: 'date'
[missing_field] ADR 99999 missing required field: 'tags'
[missing_section] ADR 99999 missing required section: '## Date'
[missing_section] ADR 99999 missing required section: '## Status'
[missing_section] ADR 99999 missing required section: '## Context'
[missing_section] ADR 99999 missing required section: '## Decision'
[missing_section] ADR 99999 missing required section: '## Consequences'
[missing_section] ADR 99999 missing required section: '## Alternatives'
[missing_section] ADR 99999 missing required section: '## References'
[missing_section] ADR 99999 missing required section: '## Participants'
PASS: Reports
missing_fieldfordatePASS: Reports
missing_fieldfortags
6. Invalid status detected¶
Create /tmp/test_adr_status.md:
cat > /tmp/test_adr_status.md <<EOF
---
id: 99999
title: Test ADR
date: 2024-01-15
status: invalid_status
tags: [architecture]
---
# ADR-99999: Test ADR
EOFRun validation:
env -u VIRTUAL_ENV uv run python -c "
from pathlib import Path
from vadocs import AdrValidator, Document, load_config, parse_frontmatter
config = load_config(Path('adr/adr_config.yaml'))
content = Path('/tmp/test_adr_status.md').read_text()
doc = Document(path=Path('/tmp/test_adr_status.md'), content=content, frontmatter=parse_frontmatter(content), doc_type='adr')
errors = AdrValidator().validate(doc, config)
for e in errors: print(f' [{e.error_type}] {e.message}')
" [invalid_status] ADR 99999 has invalid status: 'invalid_status' (valid: accepted, deprecated, proposed, rejected, superseded)
[missing_section] ADR 99999 missing required section: '## Date'
[missing_section] ADR 99999 missing required section: '## Status'
[missing_section] ADR 99999 missing required section: '## Context'
[missing_section] ADR 99999 missing required section: '## Decision'
[missing_section] ADR 99999 missing required section: '## Consequences'
[missing_section] ADR 99999 missing required section: '## Alternatives'
[missing_section] ADR 99999 missing required section: '## References'
[missing_section] ADR 99999 missing required section: '## Participants'
cat > /tmp/test_adr_tag.md <<EOF
---
id: 99999
title: Test ADR
date: 2024-01-15
status: accepted
tags: [nonexistent_tag]
---
# ADR-99999: Test ADR
EOFRun validation:
env -u VIRTUAL_ENV uv run python -c "
from pathlib import Path
from vadocs import AdrValidator, Document, load_config, parse_frontmatter
config = load_config(Path('adr/adr_config.yaml'))
content = Path('/tmp/test_adr_tag.md').read_text()
doc = Document(path=Path('/tmp/test_adr_tag.md'), content=content, frontmatter=parse_frontmatter(content), doc_type='adr')
errors = AdrValidator().validate(doc, config)
for e in errors: print(f' [{e.error_type}] {e.message}')
" [invalid_tag] ADR 99999 has invalid tag: 'nonexistent_tag' (valid: architecture, ci, context_management, devops, documentation, governance, hardware, model, security, testing, workflow)
[missing_section] ADR 99999 missing required section: '## Date'
[missing_section] ADR 99999 missing required section: '## Status'
[missing_section] ADR 99999 missing required section: '## Context'
[missing_section] ADR 99999 missing required section: '## Decision'
[missing_section] ADR 99999 missing required section: '## Consequences'
[missing_section] ADR 99999 missing required section: '## Alternatives'
[missing_section] ADR 99999 missing required section: '## References'
[missing_section] ADR 99999 missing required section: '## Participants'
PASS: Reports
invalid_tagerror
Result¶
Date: 2026-02-04
Tester: rudakow
All criteria passed:
YES
NO
Notes: Before conducting the PoC, these actions have been done: Creating Spoke Packages.