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
64 changes: 64 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Docs

on:
push:
branches: [main]
paths:
- 'docs/**'
- '*.md'
- 'mkdocs.yml'
- 'requirements-docs.txt'
- '.github/workflows/docs.yml'
pull_request:
branches: [main]
paths:
- 'docs/**'
- '*.md'
- 'mkdocs.yml'
- 'requirements-docs.txt'
- '.github/workflows/docs.yml'
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Docs site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install docs toolchain
run: pip install -r requirements-docs.txt
- name: Build docs (strict)
run: mkdocs build --strict --site-dir site
- name: Docs site actually contains its documents
# A green mkdocs build does not mean the pages have anything in them.
# devrepro-doctor published empty pages for months because every
# snippet include silently resolved to nothing.
run: python scripts/check_docs_site.py --site site
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
if: github.ref == 'refs/heads/main'
with:
path: site

deploy:
name: Deploy docs
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md),
<!-- related-projects -->
## Documentation

Browsable at **<https://webdevsamran.github.io/api-verity-lab/>**, or as files here:

| Document | What it covers |
|---|---|
| [ARCHITECTURE.md](ARCHITECTURE.md) | The normalized contract model every engine reads, and how change ids are built |
Expand Down
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "ARCHITECTURE.md"
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "CHANGELOG.md"
1 change: 1 addition & 0 deletions docs/code-of-conduct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "CODE_OF_CONDUCT.md"
1 change: 1 addition & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "CONTRIBUTING.md"
42 changes: 42 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# api-verity-lab

**Unified API contract governance, breaking-change analysis, schema-driven
testing, runtime drift detection, traffic replay and performance regression
for OpenAPI, AsyncAPI, GraphQL and gRPC.**

Every supported spec format compiles into one normalized contract model, and
every engine downstream reads that model rather than the original document.
That is what lets a breaking-change rule, a fuzz generator and a drift check
agree about what an operation is.

## Start here

| If you want to | Read |
|---|---|
| Understand the design | [Architecture](architecture.md) |
| Know which rule fired, and why | [Rule catalog](rule-catalog.md) |
| Know what each format supports | [Spec support](spec-support.md) · [Protocol support](protocol-support.md) |
| Gate a pipeline on contract changes | [CI contract gate](ci.md) |
| Use it as a library | [SDK](sdk.md) |
| Branch on results in a script | [Exit codes](exit-codes.md) |
| Judge whether it is ready for you | [Capability status](capability-status.md) · [Product gaps](product-gaps.md) |

## What this project promises about its own output

It is a verification tool, so one class of bug matters more than the rest: the
tool asserting something it did not establish. Three habits follow, and each is
enforced in CI rather than intended.

- **Never assert what the run did not establish.** A field that cannot be
derived reads `unknown`, with a reason. Provenance in an artifact that gates
someone else's build has to be earned.
- **Documented output is captured, never written.** The examples in the README
come from real runs; the rule catalogue is generated from the code; the
competitive table is rendered from committed API data. CI fails when a
generated file and its source disagree.
- **Counts are derived.** Rule totals, command counts and page counts come
from the source, not from prose that was true once.

The [rule catalog](rule-catalog.md) is generated. The
[competitive analysis](competitive-analysis.md) is rendered from data fetched
by a checked-in script. If either disagrees with its source, the build fails.
1 change: 1 addition & 0 deletions docs/product-gaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "PRODUCT_GAPS.md"
1 change: 1 addition & 0 deletions docs/protocol-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "PROTOCOL_SUPPORT.md"
1 change: 1 addition & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "ROADMAP.md"
1 change: 1 addition & 0 deletions docs/safety-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "SAFETY_MODEL.md"
1 change: 1 addition & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--8<-- "SECURITY.md"
77 changes: 77 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
site_name: api-verity-lab
site_description: >
Unified API contract governance, breaking-change analysis, schema-driven
testing, runtime drift detection, traffic replay and performance regression
for OpenAPI, AsyncAPI, GraphQL and gRPC.
site_url: https://webdevsamran.github.io/api-verity-lab/
repo_url: https://github.com/webdevsamran/api-verity-lab
repo_name: webdevsamran/api-verity-lab

theme:
name: material
palette:
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/weather-night
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/weather-sunny
name: Switch to dark mode
features:
- navigation.sections
- navigation.expand
- content.code.copy
- search.highlight
icon:
logo: material/check-decagram

nav:
- Home: index.md
- Architecture: architecture.md
- Roadmap: roadmap.md
- Reference:
- Rule catalog: rule-catalog.md
- Spec support: spec-support.md
- Protocol support: protocol-support.md
- Exit codes: exit-codes.md
- Guides:
- CI contract gate: ci.md
- SDK: sdk.md
- Self-hosting: self-hosting.md
- Workflow authoring: workflow-authoring.md
- Status:
- Capability status: capability-status.md
- Product gaps: product-gaps.md
- Competitive analysis: competitive-analysis.md
- Project:
- Contributing: contributing.md
- Security policy: security.md
- Safety model: safety-model.md
- Privacy: privacy.md
- Changelog: changelog.md
- Code of conduct: code-of-conduct.md

markdown_extensions:
- admonition
- pymdownx.superfences
- pymdownx.snippets:
# Repo root, so an include can reach ARCHITECTURE.md etc. A `../`
# path escapes base_path and pymdownx refuses it -- which is exactly
# how devrepro-doctor published empty pages for months.
base_path: .
check_paths: true
- tables
- toc:
permalink: true

validation:
omitted_files: warn
absolute_links: warn
links:
# Root documents included by snippet carry links relative to the repository
# root, which is correct where they are mostly read. check_paths above is
# the check that matters, and it fails the build.
not_found: info
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ exclude_lines = [
]

[tool.ruff.lint.per-file-ignores]
"scripts/check_docs_site.py" = ["S603", "RUF100"]
# Same shared-script situation: the gh endpoint is built from the hardcoded
# REPOS list above it, not from user input.
"scripts/fetch_competitor_meta.py" = ["S603", "S607", "RUF100"]
Expand Down
3 changes: 3 additions & 0 deletions requirements-docs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Pinned to a minor range so a docs build is reproducible. mkdocs-material is
# MIT (verified against PyPI classifiers for 9.5, 9.6, 9.7.0 and 9.7.7).
mkdocs-material>=9.7.7,<9.8
111 changes: 111 additions & 0 deletions scripts/check_docs_site.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Assert the built docs site actually contains its documents.

`mkdocs build --strict` passing does not mean the site has content in it. This
project's site was green while publishing empty pages: every one of its nine
`--8<--` snippet includes used a `../` path, which pymdownx refuses because it
escapes `base_path`, and `check_paths: false` made that refusal silent. The
Architecture page shipped with 68 words of navigation chrome and none of
ARCHITECTURE.md.

A build that succeeds while producing nothing is the same defect this
repository has fixed twice elsewhere -- a CI step named for a check it never
performed. So the site is checked for content, not just for exit status.

python scripts/check_docs_site.py # build, then verify
python scripts/check_docs_site.py --site DIR # verify an existing build
"""

from __future__ import annotations

import argparse
import re
import subprocess
import sys
import tempfile
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent

#: A page carrying only navigation chrome lands around 60-70 words.
#:
#: Two thresholds rather than one, because a flat number is the wrong
#: instrument: api-verity-lab has legitimately terse pages (docs/sdk.md is 18
#: lines, mostly a code block) that a single 120-word bar reported as broken.
#: The defect being guarded is an *include* resolving to nothing, so
#: include-backed pages get the strict bar and everything else only has to be
#: non-blank.
MIN_WORDS_INCLUDED = 120
MIN_WORDS_ANY = 40

TAG = re.compile(r"<[^>]+>")


def words_in(html: str) -> int:
body = html.split("<article", 1)[-1].split("</article>", 1)[0]
return len(TAG.sub(" ", body).split())


def build(into: Path) -> None:
result = subprocess.run( # noqa: S603 - fixed argv, no shell, no user input
[sys.executable, "-m", "mkdocs", "build", "--strict", "--site-dir", str(into)],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(result.stderr or result.stdout, file=sys.stderr)
raise SystemExit("mkdocs build failed")


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--site", type=Path, default=None)
args = parser.parse_args()

with tempfile.TemporaryDirectory() as tmp:
site = args.site or Path(tmp) / "site"
if args.site is None:
build(site)

pages = sorted(site.rglob("index.html"))
if not pages:
print("no pages were built", file=sys.stderr)
return 1

# Which source pages are built from a snippet include.
included = {
path.stem
for path in (ROOT / "docs").rglob("*.md")
if "--8<--" in path.read_text(encoding="utf-8", errors="replace")
}

thin: list[tuple[str, int, str]] = []
for page in pages:
name = page.parent.relative_to(site).as_posix() or "index"
if name.startswith(("assets", "search")):
continue
count = words_in(page.read_text(encoding="utf-8", errors="replace"))
stem = name.rsplit("/", 1)[-1] or "index"
is_included = stem in included
floor = MIN_WORDS_INCLUDED if is_included else MIN_WORDS_ANY
if count < floor:
why = "include resolved to nothing" if is_included else "page is blank"
thin.append((name, count, why))

if thin:
print(
f"{len(thin)} page(s) built with almost no content -- a snippet "
"include is probably resolving to nothing:",
file=sys.stderr,
)
for name, count, why in thin:
print(f" {name}: {count} words -- {why}", file=sys.stderr)
return 1

print(f"ok {len(pages)} pages built, all carrying real content")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading