feat: implement institutional governance and recovery architecture
This commit is contained in:
@@ -16,6 +16,7 @@ META_ROOT = Path(__file__).resolve().parents[2]
|
||||
MODULE_NAME_PATTERN = re.compile(
|
||||
r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*"
|
||||
)
|
||||
REQUIRED_DOCUMENTATION_TYPES = frozenset({"admin", "user"})
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -26,6 +27,14 @@ def main() -> int:
|
||||
default=None,
|
||||
help="Directory containing the GovOPlaN repositories. Defaults to repositories.json default_parent.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--require-architecture",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Reject every module that has not adopted the versioned architecture "
|
||||
"declaration. GovOPlaN release and focused checks enable this gate."
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
catalog = json.loads((META_ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||
@@ -90,6 +99,36 @@ def main() -> int:
|
||||
)
|
||||
continue
|
||||
|
||||
documented_types = {
|
||||
documentation_type
|
||||
for topic in manifest.documentation
|
||||
for documentation_type in topic.documentation_types
|
||||
}
|
||||
missing_documentation_types = sorted(REQUIRED_DOCUMENTATION_TYPES - documented_types)
|
||||
if missing_documentation_types:
|
||||
errors.append(
|
||||
f"{repository_name}: module {manifest.id!r} is missing static documentation for "
|
||||
f"{', '.join(missing_documentation_types)}; add manifest DocumentationTopic entries "
|
||||
"even when runtime documentation providers are registered"
|
||||
)
|
||||
continue
|
||||
|
||||
repository_root = manifest_path.parents[3]
|
||||
if manifest.architecture is None:
|
||||
if args.require_architecture:
|
||||
errors.append(
|
||||
f"{repository_name}: module {manifest.id!r} has no architecture declaration"
|
||||
)
|
||||
continue
|
||||
else:
|
||||
errors.extend(
|
||||
_architecture_evidence_errors(
|
||||
repository_name=repository_name,
|
||||
repository_root=repository_root,
|
||||
manifest=manifest,
|
||||
)
|
||||
)
|
||||
|
||||
manifests.append(manifest)
|
||||
|
||||
if errors:
|
||||
@@ -109,6 +148,11 @@ def main() -> int:
|
||||
f"Manifest registry check passed: {len(snapshot.manifests)} manifests "
|
||||
f"from {len(manifest_sources)} source files."
|
||||
)
|
||||
declared = sum(manifest.architecture is not None for manifest in manifests)
|
||||
print(
|
||||
f"Architecture declaration coverage: {declared}/{len(manifests)} modules "
|
||||
f"({(declared / len(manifests) * 100):.1f}%)."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -120,5 +164,53 @@ def _module_entry_points(pyproject_path: Path) -> dict[str, str]:
|
||||
return {str(name): str(target) for name, target in values.items()}
|
||||
|
||||
|
||||
def _architecture_evidence_errors(
|
||||
*,
|
||||
repository_name: str,
|
||||
repository_root: Path,
|
||||
manifest: object,
|
||||
) -> list[str]:
|
||||
architecture = getattr(manifest, "architecture", None)
|
||||
if architecture is None:
|
||||
return []
|
||||
errors: list[str] = []
|
||||
references = [
|
||||
(f"{item.kind} evidence", item.reference)
|
||||
for item in architecture.evidence
|
||||
]
|
||||
for category in ("migration", "upgrade", "recovery", "security", "operations"):
|
||||
references.extend(
|
||||
(f"{category} documentation", reference)
|
||||
for reference in getattr(architecture.documentation, category)
|
||||
)
|
||||
for label, reference in references:
|
||||
if not _looks_like_repository_reference(reference):
|
||||
continue
|
||||
candidate = (repository_root / reference).resolve()
|
||||
try:
|
||||
candidate.relative_to(repository_root.resolve())
|
||||
except ValueError:
|
||||
errors.append(
|
||||
f"{repository_name}: {label} escapes the repository: {reference!r}"
|
||||
)
|
||||
continue
|
||||
if not candidate.exists():
|
||||
errors.append(
|
||||
f"{repository_name}: {label} does not exist: {reference!r}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _looks_like_repository_reference(reference: str) -> bool:
|
||||
normalized = reference.strip()
|
||||
if not normalized or "://" in normalized:
|
||||
return False
|
||||
return (
|
||||
"/" in normalized
|
||||
or normalized.startswith("README")
|
||||
or normalized.endswith((".md", ".py", ".json", ".yaml", ".yml"))
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user