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
121 changes: 121 additions & 0 deletions .github/tests/test_docs_contract.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import json
import re
import unittest
from pathlib import Path


REPO = Path(__file__).resolve().parents[2]


MARKDOWN_LINK = re.compile(r"(?<!!)\[[^\]]+\]\(([^)]+)\)")


def unresolved_markdown_links(path: Path) -> list[str]:
failures: list[str] = []
for target in MARKDOWN_LINK.findall(path.read_text()):
target = target.strip().split(" ", 1)[0].strip("<>")
if target.startswith(("http://", "https://", "mailto:", "#")):
continue
relative = target.split("#", 1)[0]
if relative and not (path.parent / relative).resolve().exists():
failures.append(target)
return failures


def yaml_scalar(value: str) -> str:
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
Expand Down Expand Up @@ -131,6 +147,111 @@ def assert_pages_contract(testcase: unittest.TestCase, workflow: str) -> None:


class DocumentationContractTests(unittest.TestCase):
def test_documentation_entrypoint_links_resolve(self) -> None:
for path in (
REPO / "README.md",
REPO / "docs" / "index.md",
REPO / "docs" / "concepts" / "index.md",
REPO / "docs" / "architecture" / "index.md",
):
self.assertEqual(unresolved_markdown_links(path), [], path)

def test_concept_and_architecture_indexes_are_complete(self) -> None:
concepts = {
"supervisory-control.md",
"control-programs-flows-and-runs.md",
"state-observation-objectives-and-targets.md",
"transitions-operators-effects-and-capabilities.md",
"authority-identity-and-delegation.md",
"prescriptions-verification-receipts-and-recovery.md",
"invocation-parameters-and-foreground-work.md",
}
architecture = {
"kernel.md",
"compiler-and-artifacts.md",
"runtime-persistence-and-control-bundles.md",
"surfaces-and-host-projections.md",
"software-delivery-domain.md",
"conformance-and-generated-evidence.md",
}
for directory, expected in (("concepts", concepts), ("architecture", architecture)):
index = (REPO / "docs" / directory / "index.md").read_text()
for name in expected:
self.assertTrue((REPO / "docs" / directory / name).exists(), name)
self.assertIn(f"({name})", index)

def test_v1_documents_are_not_retained_as_current_or_historical_authority(self) -> None:
for name in (
"boatstack-kernel.md",
"boatstack-closure-report.md",
"boatstack-v1-authority-inventory.md",
):
self.assertFalse(any((REPO / "docs").rglob(name)), name)
history = (REPO / "docs" / "history" / "index.md").read_text().lower()
self.assertIn("does not define current", history)

def test_generated_architecture_evidence_declares_ownership(self) -> None:
ownership = (REPO / "docs" / "generated-files.md").read_text()
generated = (
"boatstack-transition-catalog.md",
"boatstack-transition-catalog.mmd",
"boatstack-standard-flow.mmd",
"boatstack-locus-safety.json",
"boatstack-locus-liveness.json",
)
for name in generated:
self.assertIn(name, ownership)
self.assertIn(f"catalog --format", ownership)
for name in generated[:3]:
text = (REPO / "docs" / "architecture" / name).read_text()[:300]
self.assertRegex(text, r"(?i)generated.*do not edit")

def test_projection_reference_matches_canonical_vocabulary_and_paths(self) -> None:
source = (REPO / "boatstack" / "internal" / "hostprojection" / "projection.go").read_text()
canonical = set(re.findall(r'^\s*\w+\s+ID\s+=\s+"([a-z]+)"', source, re.MULTILINE))
self.assertEqual(canonical, {"codex", "claude", "cursor", "gemini"})
generated = (REPO / "docs" / "generated-files.md").read_text().lower()
surfaces = (REPO / "docs" / "architecture" / "surfaces-and-host-projections.md").read_text().lower()
for projection in canonical:
self.assertIn(projection, generated)
self.assertIn(projection, surfaces)
for path in (
".agents/skills/<program>-<entry>/skill.md",
".claude/skills/<program>-<entry>/skill.md",
".cursor/commands/<program>-<entry>.md",
".gemini/skills/<program>-<entry>/skill.md",
):
self.assertIn(path, generated)

def test_public_docs_exclude_private_and_volatile_content(self) -> None:
public = "\n".join(
path.read_text(errors="replace")
for path in (REPO / "docs").rglob("*.md")
) + (REPO / "README.md").read_text()
for forbidden in (
"/Users/",
"ChatGPT conversation",
"local-LLM",
):
self.assertNotIn(forbidden, public)
for concept in (REPO / "docs" / "concepts").glob("*.md"):
self.assertIsNone(
re.search(r"\b\d+\s+(?:executable\s+)?transitions\b", concept.read_text(), re.IGNORECASE),
concept,
)

def test_control_program_schema_reference_matches_sdk_constant(self) -> None:
source = (REPO / "packages" / "boatstack" / "src" / "index.ts").read_text()
revision = re.search(r"CONTROL_PROGRAM_SCHEMA_REVISION = (\d+)", source)
self.assertIsNotNone(revision)
reference = (REPO / "docs" / "control-program-ir.md").read_text()
self.assertIn(f"`schema_revision: {revision.group(1)}`", reference)

def test_all_typedoc_project_documents_exist(self) -> None:
config = json.loads((REPO / "typedoc.json").read_text())
for document in config["projectDocuments"]:
self.assertTrue((REPO / document).is_file(), document)

def test_required_ci_validates_flow_sdk_and_documentation(self) -> None:
ci = (REPO / ".github" / "workflows" / "ci.yml").read_text()
steps = workflow_jobs(ci)["flow-sdk"]["steps"]
Expand Down
Loading
Loading