PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
certify_documentation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Create a commit-scoped documentation certification after strict validation."""
3
4from __future__ import annotations
5
6import argparse
7import json
8import datetime as dt
9import subprocess
10import sys
11from pathlib import Path
12
13
14REPO_ROOT = Path(__file__).resolve().parents[2]
15
16
17def run(command: list[str], label: str) -> None:
18 """!
19 @brief Run one certification gate and stream its output.
20 @param[in] command Executable and arguments for the gate.
21 @param[in] label Human-readable gate label.
22 @return None.
23 """
24
25 print(f"==> {label}", flush=True)
26 completed = subprocess.run(command, cwd=REPO_ROOT, check=False)
27 if completed.returncode != 0:
28 raise RuntimeError(f"{label} failed with exit code {completed.returncode}.")
29
30
31def git_output(*args: str) -> str:
32 """!
33 @brief Return trimmed output from one Git command.
34 @param[in] args Arguments passed after `git`.
35 @return Decoded, trimmed Git stdout.
36 """
37
38 return subprocess.check_output(["git", *args], cwd=REPO_ROOT, text=True).strip()
39
40
42 """!
43 @brief Require that HEAD names a clean, commit-scoped source revision.
44 @return Full SHA of the clean HEAD revision.
45 """
46
47 if git_output("status", "--porcelain"):
48 raise RuntimeError(
49 "Documentation certification requires a clean working tree so its result can be "
50 "claimed for one commit. Commit or stash changes, then rerun."
51 )
52 return git_output("rev-parse", "HEAD")
53
54
56 """!
57 @brief Fail certification when Doxygen reports a warning.
58 @return None.
59 """
60
61 warning_log = REPO_ROOT / "logs" / "doxygen.warnings"
62 warnings = warning_log.read_text(encoding="utf-8").strip() if warning_log.exists() else ""
63 if warnings:
64 raise RuntimeError(
65 "Doxygen emitted warnings; documentation cannot be certified.\n" + warnings
66 )
67
68
70 """!
71 @brief Describe how much of the invariant surface has an automated checker.
72
73 Most invariant contracts are tracked rather than enforced, so the certificate must
74 not imply that every guarantee is verified.
75 @return Human-readable contract scope statement.
76 """
77 path = REPO_ROOT / "tests" / "tooling" / "contract_registry.json"
78 try:
79 contracts = json.loads(path.read_text(encoding="utf-8"))["contracts"]
80 except (OSError, ValueError, KeyError):
81 return "scope unknown (contract registry unreadable)"
82 enforced = [c["id"] for c in contracts if c["status"] == "enforced"]
83 other = [c for c in contracts if c["status"] != "enforced"]
84 return (
85 f"{len(enforced)} enforced and verified; "
86 f"{len(other)} TRACKED OR PLANNED without an automated checker"
87 )
88
89
91 """!
92 @brief Describe how much of the capability surface has enforced documentation coverage.
93
94 The certificate must not imply that every capability is documented while families
95 are still being backfilled, so it names what is enforced and what is advisory.
96 @return Human-readable coverage scope statement.
97 """
98 registry_path = REPO_ROOT / "tests" / "tooling" / "capability_families.json"
99 try:
100 families = json.loads(registry_path.read_text(encoding="utf-8"))["families"]
101 except (OSError, ValueError, KeyError):
102 return "scope unknown (capability registry unreadable)"
103 enforced = sorted(f["id"] for f in families if f.get("coverage_enforced"))
104 advisory = sorted(f["id"] for f in families if not f.get("coverage_enforced"))
105 if not advisory:
106 return f"enforced for all {len(enforced)} registered families"
107 return (
108 f"enforced for {', '.join(enforced) or 'no families'}; "
109 f"ADVISORY ONLY for {', '.join(advisory)} (backfill pending)"
110 )
111
112
113def freshness_scope() -> str:
114 """!
115 @brief Describe the four freshness states separately.
116
117 @details Mechanical verification, a current fingerprint, a soft suspicion, and a
118 human review are four different claims. The certificate must not blur
119 them: a matching digest records that a review happened, never that the
120 prose it covers is correct.
121 @return Human-readable freshness statement.
122 """
123 path = REPO_ROOT / "tests" / "tooling" / "freshness_manifest.json"
124 try:
125 surfaces = json.loads(path.read_text(encoding="utf-8"))["surfaces"]
126 except (OSError, ValueError, KeyError):
127 return "scope unknown (freshness manifest unreadable)"
128 import hashlib
129
130 def digest(paths: list) -> str:
131 """!
132 @brief Digest matching audit_freshness.digest_of.
133 @param[in] paths Repository-relative paths.
134 @return Hex digest with algorithm prefix.
135 """
136 accumulator = hashlib.sha256()
137 for relative in sorted(paths):
138 accumulator.update(relative.encode("utf-8"))
139 accumulator.update(b"\0")
140 accumulator.update((REPO_ROOT / relative).read_bytes())
141 accumulator.update(b"\0")
142 return f"sha256:{accumulator.hexdigest()}"
143
144 hard_current = soft_current = suspect = never = 0
145 for surface in surfaces:
146 paths = ([surface["artifact"]] if surface["tier"] == "hard"
147 else list(surface.get("watched_paths", [])))
148 attested = surface.get("attested_digest")
149 if attested in (None, "unattested"):
150 never += 1
151 elif attested == digest(paths):
152 if surface["tier"] == "hard":
153 hard_current += 1
154 else:
155 soft_current += 1
156 else:
157 suspect += 1
158 return (
159 f"{hard_current} hard-current (normalized artifact unchanged since review); "
160 f"{soft_current} soft-current (watched sources unchanged since review); "
161 f"{suspect} SUSPECT; {never} NEVER ATTESTED against their sources"
162 )
163
164
165def write_certificate(sha: str, runtime_checked: bool) -> Path:
166 """!
167 @brief Write a reproducible certification record for one validated revision.
168 @param[in] sha Full Git revision validated by the certification gates.
169 @param[in] runtime_checked Whether the PETSc/MPI runtime suite was included.
170 @return Path of the generated certificate.
171 """
172
173 certificate = REPO_ROOT / "logs" / f"documentation-certificate-{sha}.md"
174 coverage_scope = capability_coverage_scope()
175 contract_scope = invariant_contract_scope()
176 freshness = freshness_scope()
177 timestamp = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat()
178 runtime_gate = (
179 "`make check-full` (including `unit-io` startup-banner and `unit-logging` contracts)"
180 if runtime_checked
181 else "not run"
182 )
183 scope_statement = (
184 "This full certification is authoritative only for the commit named above. "
185 "Any source, configuration, example, or documentation change requires a new certification.\n"
186 if runtime_checked
187 else "This structural certification is authoritative only for the commit named above and "
188 "does not certify PETSc/MPI runtime behavior. A pre-push policy may reuse a recent full "
189 "certificate only when it establishes that runtime-relevant files are unchanged.\n"
190 )
191 certificate.write_text(
192 "# PICurv Documentation Certification\n\n"
193 f"- Certified commit: `{sha}`\n"
194 f"- Certified at (UTC): `{timestamp}`\n"
195 "- Portable shared agent instructions and skill parity: passed\n"
196 "- Markdown links (all tracked and non-ignored Markdown): passed\n"
197 "- Field catalog identity and layout: passed\n"
198 "- Public-header and implementation comment audit: passed\n"
199 "- User-facing C/Python reporting audit: passed\n"
200 "- Starter template, example, and configuration audit: passed\n"
201 "- Generic documentation-expansion debris audit: passed\n"
202 f"- Capability parity (full source chain): passed\n"
203 f"- Invariant contract registry: {contract_scope}\n"
204 f"- Tier-2 documentation coverage: {coverage_scope}\n"
205 "- Page-type coverage (every published page typed): passed\n"
206 "- Subsystem lifecycle obligations: passed\n"
207 f"- Documentation freshness: {freshness}\n"
208 "- PETSc option-ingress manifest audit: passed\n"
209 "- Example/template and configuration regression suite: passed\n"
210 "- Doxygen HTML build: passed with zero warnings\n"
211 "- Published-site URLs and navigation (validated against generated HTML): passed\n"
212 f"- PETSc/MPI runtime validation: {runtime_gate}\n\n"
213 + scope_statement
214 + "\nThese lines record four different kinds of claim and must not be conflated. A "
215 "gate that\npassed is **mechanically verified**. A hard-current surface means the "
216 "normalized artifact a\npage describes has not changed since someone last compared "
217 "them - not that the page is\ncorrect. A soft suspicion is advisory. A never-attested "
218 "surface means no such comparison has\nhappened at all. **Scientific and visual review "
219 "by the repository owner is a separate,\nhuman act that no gate here performs.**\n",
220 encoding="utf-8",
221 )
222 return certificate
223
224
225def parse_args(argv: list[str]) -> argparse.Namespace:
226 """!
227 @brief Parse documentation-certification command-line arguments.
228 @param[in] argv Arguments excluding the executable name.
229 @return Parsed command-line namespace.
230 """
231
232 parser = argparse.ArgumentParser(
233 description="Validate and certify PICurv documentation for the current clean Git commit."
234 )
235 parser.add_argument(
236 "--runtime",
237 action="store_true",
238 help=(
239 "also run the PETSc/MPI `make check-full` runtime validation gate "
240 "(including startup-banner and logging contracts)"
241 ),
242 )
243 parser.add_argument(
244 "--no-certificate",
245 action="store_true",
246 help="run the gates without writing logs/documentation-certificate-<sha>.md",
247 )
248 return parser.parse_args(argv)
249
250
251def main(argv: list[str] | None = None) -> int:
252 """!
253 @brief Run the commit-scoped documentation certification workflow.
254 @param[in] argv Optional command-line arguments excluding the executable name.
255 @return Process status code.
256 """
257
258 args = parse_args(sys.argv[1:] if argv is None else argv)
259 try:
261 run([sys.executable, "tests/tooling/audit_agent_setup.py"], "portable agent setup")
262 run([sys.executable, "tests/tooling/check_markdown_links.py"], "Markdown links")
263 run([sys.executable, "tests/tooling/audit_function_docs.py"], "function documentation")
264 run([sys.executable, "tests/tooling/audit_user_facing_reporting.py"], "user-facing reporting")
265 run([sys.executable, "tests/tooling/audit_starter_content.py"], "starter templates and configuration")
266 run([sys.executable, "tests/tooling/audit_generic_expansion.py"], "generic documentation-expansion debris")
267 run([sys.executable, "tests/tooling/audit_path_literals.py"], "unmanaged run-path literals")
268 run([sys.executable, "tests/tooling/audit_capability_coverage.py"], "capability parity and documentation coverage")
269 run([sys.executable, "tests/tooling/audit_family_census.py"], "capability family census")
270 run([sys.executable, "tests/tooling/generate_cli_reference.py", "--check"], "generated CLI reference")
271 run([sys.executable, "tests/tooling/audit_ingress.py"], "option-ingress manifest")
272 run(
273 [
274 sys.executable,
275 "-m",
276 "pytest",
277 "-q",
278 "tests/test_repo_consistency.py",
279 "tests/test_config_regressions.py",
280 "tests/test_capability_tooling.py",
281 ],
282 "example/template, configuration, and capability-tooling validation",
283 )
284 run(["make", "--no-print-directory", "build-docs"], "Doxygen HTML build")
286 # Runs after the build: it validates canonical URLs and navigation tabs against the
287 # generated HTML, so an excluded or renamed page cannot pass on a source declaration.
288 run([sys.executable, "tests/tooling/audit_docs_site.py"], "published-site URLs and navigation")
289 run([sys.executable, "tests/tooling/audit_page_types.py"], "page-type coverage")
290 run([sys.executable, "tests/tooling/audit_field_catalog.py"],
291 "field catalog identity and layout")
292 run([sys.executable, "tests/tooling/audit_inline_choices.py"],
293 "named public choice sets")
294 run([sys.executable, "tests/tooling/audit_subsystem_lifecycle.py"],
295 "subsystem lifecycle obligations")
296 run([sys.executable, "tests/tooling/audit_freshness.py"],
297 "documentation freshness (hard blocking, soft advisory)")
298 run([sys.executable, "tests/tooling/audit_contracts.py"], "invariant contract registry")
299 if args.runtime:
300 run(
301 ["make", "--no-print-directory", "check-full"],
302 "PETSc/MPI runtime validation (including startup-banner and logging contracts)",
303 )
304 if not args.no_certificate:
305 certificate = write_certificate(sha, args.runtime)
306 certification_kind = "full" if args.runtime else "structural"
307 print(f"{certification_kind.capitalize()} documentation certification through commit {sha}: {certificate}")
308 return 0
309 except (RuntimeError, subprocess.CalledProcessError) as error:
310 print(f"Documentation certification failed: {error}", file=sys.stderr)
311 return 1
312
313
314if __name__ == "__main__":
315 raise SystemExit(main())
None run(list[str] command, str label)
Run one certification gate and stream its output.
None ensure_empty_doxygen_warning_log()
Fail certification when Doxygen reports a warning.
str require_clean_revision()
Require that HEAD names a clean, commit-scoped source revision.
argparse.Namespace parse_args(list[str] argv)
Parse documentation-certification command-line arguments.
str invariant_contract_scope()
Describe how much of the invariant surface has an automated checker.
str freshness_scope()
Describe the four freshness states separately.
int main(list[str]|None argv=None)
Run the commit-scoped documentation certification workflow.
Path write_certificate(str sha, bool runtime_checked)
Write a reproducible certification record for one validated revision.
str capability_coverage_scope()
Describe how much of the capability surface has enforced documentation coverage.
str git_output(*str args)
Return trimmed output from one Git command.
Head of a generic C-style linked list.
Definition variables.h:475