md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
138 lines
4.8 KiB
YAML
138 lines
4.8 KiB
YAML
name: Docs
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- "**/*.md"
|
|
- ".github/ISSUE_TEMPLATE/**"
|
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
- ".github/workflows/docs.yml"
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "**/*.md"
|
|
- ".github/ISSUE_TEMPLATE/**"
|
|
- ".github/PULL_REQUEST_TEMPLATE.md"
|
|
- ".github/workflows/docs.yml"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: docs-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
links:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- name: Validate active relative Markdown links
|
|
run: |
|
|
python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
# Validate every Markdown file in the documentation surface. Globbing
|
|
# (rather than a hand-maintained list) means new docs are covered
|
|
# automatically and phantom links cannot slip in. Skip vendored trees.
|
|
skip_dirs = {".git", "node_modules", ".venv", ".uv-cache"}
|
|
files = sorted(
|
|
p
|
|
for p in Path(".").rglob("*.md")
|
|
if not any(part in skip_dirs for part in p.parts)
|
|
)
|
|
|
|
missing = []
|
|
for path in files:
|
|
if not path.exists():
|
|
continue
|
|
text = path.read_text()
|
|
active = re.sub(r"<!--.*?-->", "", text, flags=re.S)
|
|
for raw in re.findall(r"\[[^\]]*\]\(([^)]+)\)", active):
|
|
link = raw.split("#", 1)[0]
|
|
if not link or link.startswith(("http://", "https://", "mailto:")):
|
|
continue
|
|
target = (path.parent / link).resolve()
|
|
try:
|
|
target.relative_to(Path.cwd().resolve())
|
|
except ValueError:
|
|
missing.append((path, raw, "outside repository"))
|
|
continue
|
|
if not target.exists():
|
|
missing.append((path, raw, "missing"))
|
|
|
|
if missing:
|
|
for path, raw, reason in missing:
|
|
print(f"{path}: {raw} -> {reason}")
|
|
sys.exit(1)
|
|
|
|
print("Active relative Markdown links resolve.")
|
|
PY
|
|
|
|
- name: Validate use-case banner links
|
|
run: |
|
|
python3 - <<'PY'
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
files = {
|
|
Path("README.md"): "## Use Cases",
|
|
Path("use-cases/README.md"): "## Use Case Catalogue",
|
|
}
|
|
|
|
failures = []
|
|
warnings = []
|
|
primary_link_pattern = re.compile(
|
|
r"^\[(?:Code|Plugin|Live Demo|Learn more)\]\(([^)]+)\)",
|
|
flags=re.M,
|
|
)
|
|
|
|
for path, heading in files.items():
|
|
text = path.read_text()
|
|
start = text.find(heading)
|
|
if start == -1:
|
|
failures.append(f"{path}: missing {heading}")
|
|
continue
|
|
|
|
table_start = text.find("<table>", start)
|
|
table_end = text.find("</table>", table_start)
|
|
if table_start == -1 or table_end == -1:
|
|
failures.append(f"{path}: missing use-case table")
|
|
continue
|
|
|
|
table = text[table_start:table_end]
|
|
cells = re.findall(r"<td[^>]*>(.*?)</td>", table, flags=re.S)
|
|
for index, cell in enumerate(cells, start=1):
|
|
title_match = re.search(r"####\s+(.+)", cell)
|
|
title = title_match.group(1).strip() if title_match else f"use case {index}"
|
|
banner_match = re.search(r"\[!\[[^\]]*\]\([^)]+\)\]\(([^)]+)\)", cell)
|
|
primary_match = primary_link_pattern.search(cell)
|
|
|
|
if not banner_match and primary_match:
|
|
warnings.append(f"{path}: {title}: primary link has no linked banner")
|
|
elif banner_match and not primary_match:
|
|
failures.append(f"{path}: {title}: missing primary link")
|
|
elif banner_match and primary_match and banner_match.group(1) != primary_match.group(1):
|
|
failures.append(
|
|
f"{path}: {title}: banner link {banner_match.group(1)} "
|
|
f"does not match primary link {primary_match.group(1)}"
|
|
)
|
|
|
|
if warnings:
|
|
print("\n".join(f"warning: {warning}" for warning in warnings))
|
|
|
|
if failures:
|
|
print("\n".join(failures))
|
|
sys.exit(1)
|
|
|
|
print("Use-case banner links match primary links.")
|
|
PY
|
|
|
|
- name: Validate issue template YAML
|
|
run: |
|
|
ruby -e 'require "yaml"; Dir[".github/ISSUE_TEMPLATE/*.yml"].sort.each { |p| YAML.load_file(p); puts "YAML ok: #{p}" }'
|