PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
audit_subsystem_lifecycle.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Enforce the documentation obligations each subsystem lifecycle status carries."""
3
4from __future__ import annotations
5
6import json
7import re
8import sys
9from pathlib import Path
10
11
12REPO_ROOT = Path(__file__).resolve().parents[2]
13RECORDS = REPO_ROOT / "tests" / "tooling" / "subsystem_records.json"
14PAGE_TYPES = REPO_ROOT / "tests" / "tooling" / "page_types.json"
15FAMILIES = REPO_ROOT / "tests" / "tooling" / "capability_families.json"
16PAGE_DIRS = ("docs/pages", "docs")
17
18# The ladder is cumulative: claiming a rung owes everything below it too. This is
19# the whole point of the gate - experimental work is never blocked by documentation
20# it cannot honestly write yet, but it also cannot skip a rung by claiming the top.
21LADDER = ("planned", "internal", "experimental", "supported")
22
23LADDER_OBLIGATIONS = {
24 "planned": ("purpose", "intended_scope", "design_owner", "not_implemented_status"),
25 "internal": ("scope_boundary", "architecture_boundary", "dependencies",
26 "developer_entry_points"),
27 "experimental": ("configuration", "selection_guidance", "observability", "limitations",
28 "safe_use_boundaries"),
29 "supported": ("evidence", "lifecycle_and_restart", "operations", "troubleshooting",
30 "examples", "complete_reference"),
31}
32
33# Statuses off the ladder. Each owes its own set; those that describe something that
34# was once built also owe the ladder up to the peak status it reached.
35TERMINAL_OBLIGATIONS = {
36 "known-defective": ("defect_disclosure", "defect_scope_record", "safe_use_boundaries",
37 "limitations"),
38 "deprecated": ("migration_path", "replacement", "compatibility_period", "removal_policy"),
39 "removed": ("history_record", "rejection_behavior"),
40}
41# Obligations that make sense only while the subsystem is unbuilt. A supported
42# subsystem does not owe a not-implemented notice.
43NON_INHERITED = ("not_implemented_status",)
44NEEDS_PEAK = ("known-defective", "deprecated", "removed")
45# `removed` documents an absence, so it does not owe the prose of what it replaced.
46INHERITS_LADDER = ("known-defective", "deprecated")
47
48VALID_STATUSES = tuple(LADDER) + tuple(TERMINAL_OBLIGATIONS)
49
50# Which status may follow which. Absent from a row means the transition is invalid:
51# a subsystem cannot re-enter the ladder below where it stood, and cannot reach
52# `removed` without first being deprecated or declared defective.
53#
54# `planned -> removed` is the one deliberate exception, and it is not a removal in the
55# same sense: nothing was ever built, so a cancelled design owes a history record and
56# a rejection behaviour rather than a migration path. Every other route to `removed`
57# passes through `deprecated` or `known-defective`.
58TRANSITIONS = {
59 None: set(VALID_STATUSES) - {"removed"},
60 "planned": {"planned", "internal", "experimental", "supported", "removed"},
61 "internal": {"internal", "experimental", "supported", "known-defective", "deprecated"},
62 "experimental": {"experimental", "supported", "known-defective", "deprecated"},
63 "supported": {"supported", "known-defective", "deprecated"},
64 "known-defective": {"known-defective", "experimental", "supported", "deprecated", "removed"},
65 "deprecated": {"deprecated", "removed"},
66 "removed": {"removed"},
67}
68
69VALID_VISIBILITY = ("internal", "public")
70
71# The concern vocabulary of 64_Documentation_Extension_Framework section 5.
72VALID_CONCERNS = (
73 "numerical_method", "user_selector", "units_and_nondimensionalization",
74 "artifact_topology", "persistent_restart_state", "determinism_and_reproducibility",
75 "mpi_distributed_execution", "external_service", "security_credentials",
76 "generated_artifact", "file_format", "destructive_scope", "backward_compatibility",
77 "scientific_verification",
78)
79
80RECORD_KEYS = {"id", "title", "status", "visibility", "previous_status", "peak_status",
81 "proposed_status", "promotion_rationale", "capability_families",
82 "obligations", "concerns", "note"}
83# A reason short enough to fit here is an evasion, not a reason.
84MIN_REASON_CHARS = 30
85FILLER_REASONS = {"n/a", "na", "none", "not applicable", "no", "-", "tbd"}
86
87
88def page_index() -> dict:
89 """!
90 @brief Every documentation page id with the anchors it defines.
91 @return Mapping of page id to the set of `@section`, `@subsection`, and `@anchor` names.
92 """
93 index: dict = {}
94 for directory in PAGE_DIRS:
95 for markdown in sorted((REPO_ROOT / directory).glob("*.md")):
96 text = markdown.read_text(encoding="utf-8")
97 match = re.search(r"^@page\s+(\S+)", text, re.M)
98 if not match or match.group(1).startswith("<"):
99 continue
100 anchors = set(re.findall(r"^@(?:section|subsection|subsubsection|anchor)\s+(\S+)",
101 text, re.M))
102 index[match.group(1)] = anchors
103 return index
104
105
106def required_obligations(record: dict) -> tuple:
107 """!
108 @brief The obligation ids a record owes.
109
110 @details A record may declare a `proposed_status` above its claimed one. That is
111 how a subsystem documented to a higher bar waits for the owner to decide
112 whether it has actually earned it: obligations are checked against the
113 proposal, so the writing is held to the higher standard, while the status
114 the documentation publishes stays conservative until a human agrees.
115 @param[in] record One subsystem record.
116 @return Ordered tuple of obligation ids.
117 """
118 status = record.get("proposed_status") or record.get("status")
119 owed: list = []
120 if status in LADDER:
121 for rung in LADDER[: LADDER.index(status) + 1]:
122 owed.extend(LADDER_OBLIGATIONS[rung])
123 else:
124 if status in INHERITS_LADDER:
125 peak = record.get("peak_status")
126 if peak in LADDER:
127 for rung in LADDER[: LADDER.index(peak) + 1]:
128 owed.extend(LADDER_OBLIGATIONS[rung])
129 owed.extend(TERMINAL_OBLIGATIONS.get(status, ()))
130 if status != "planned":
131 owed = [key for key in owed if key not in NON_INHERITED]
132 # A status may re-owe something a lower rung already required; keep it once.
133 return tuple(dict.fromkeys(owed))
134
135
136def check_satisfaction(context: str, key: str, spec, pages: dict, published: set,
137 public: bool) -> list:
138 """!
139 @brief Verify one obligation or concern is genuinely answered.
140
141 @details An answer is a documentation reference that resolves, or a stated reason
142 for non-applicability, or a literal value where the obligation is a fact
143 rather than prose. An empty or filler answer is a violation.
144 @param[in] context Record id, for messages.
145 @param[in] key Obligation or concern id.
146 @param[in] spec The declared answer.
147 @param[in] pages Page index from page_index().
148 @param[in] published Ids of the pages the site publishes.
149 @param[in] public Whether the subsystem is publicly visible.
150 @return List of violation strings.
151 """
152 if not isinstance(spec, dict):
153 return [f"{context}: '{key}' must be an object, got {type(spec).__name__}"]
154 problems = []
155 if "not_applicable" in spec:
156 reason = str(spec["not_applicable"]).strip()
157 if reason.lower().rstrip(".") in FILLER_REASONS or len(reason) < MIN_REASON_CHARS:
158 problems.append(
159 f"{context}: '{key}' is declared not applicable without a stated reason "
160 f"({reason!r}). A reasoned N/A is accepted; a bare one is not"
161 )
162 return problems
163 if "value" in spec:
164 if not str(spec["value"]).strip():
165 problems.append(f"{context}: '{key}' declares an empty value")
166 return problems
167 page = spec.get("page")
168 if not page:
169 problems.append(
170 f"{context}: '{key}' is unsatisfied - give it a page reference, a value, or a "
171 f"reasoned not_applicable"
172 )
173 return problems
174 if page not in pages:
175 problems.append(f"{context}: '{key}' cites page '{page}', which does not exist")
176 return problems
177 anchor = spec.get("anchor")
178 if anchor and anchor not in pages[page]:
179 problems.append(
180 f"{context}: '{key}' cites anchor '{anchor}' on page '{page}', which does not "
181 f"define it"
182 )
183 if public and page not in published:
184 problems.append(
185 f"{context}: '{key}' cites '{page}', which is not a published page, but the "
186 f"subsystem is publicly visible"
187 )
188 return problems
189
190
191def validate(records: list, pages: dict, families: dict, published: set) -> list:
192 """!
193 @brief Validate every subsystem record against the lifecycle contract.
194 @param[in] records Subsystem records.
195 @param[in] pages Page index from page_index().
196 @param[in] families Capability family metadata, keyed by family id.
197 @param[in] published Ids of the pages the site publishes.
198 @return List of violation strings; empty means the contract holds.
199 """
200 problems: list = []
201 seen: set = set()
202 for record in records:
203 identifier = record.get("id")
204 if not identifier:
205 problems.append("a record declares no id")
206 continue
207 if identifier in seen:
208 problems.append(f"{identifier}: declared more than once")
209 seen.add(identifier)
210
211 unknown = set(record) - RECORD_KEYS
212 if unknown:
213 problems.append(f"{identifier}: unknown field(s) {sorted(unknown)}")
214
215 status = record.get("status")
216 if status not in VALID_STATUSES:
217 problems.append(
218 f"{identifier}: status {status!r} is not one of {list(VALID_STATUSES)}"
219 )
220 continue
221
222 visibility = record.get("visibility")
223 if visibility not in VALID_VISIBILITY:
224 problems.append(
225 f"{identifier}: visibility {visibility!r} is not one of "
226 f"{list(VALID_VISIBILITY)}"
227 )
228 continue
229 if status == "internal" and visibility != "internal":
230 problems.append(
231 f"{identifier}: status 'internal' contradicts visibility 'public'; a "
232 f"subsystem the user can reach is at least experimental"
233 )
234
235 previous = record.get("previous_status", None)
236 if previous is not None and previous not in VALID_STATUSES:
237 problems.append(f"{identifier}: previous_status {previous!r} is not a valid status")
238 elif status not in TRANSITIONS[previous]:
239 problems.append(
240 f"{identifier}: {previous or 'a new record'} -> '{status}' is not a valid "
241 f"lifecycle transition (allowed: {sorted(TRANSITIONS[previous])})"
242 )
243
244 proposed = record.get("proposed_status")
245 if proposed is not None:
246 if proposed not in LADDER:
247 problems.append(
248 f"{identifier}: proposed_status {proposed!r} must name a rung of "
249 f"{list(LADDER)}"
250 )
251 elif status not in LADDER or LADDER.index(proposed) <= LADDER.index(status):
252 problems.append(
253 f"{identifier}: proposed_status '{proposed}' is not above the claimed "
254 f"status '{status}'; a proposal that is not a promotion is noise"
255 )
256 elif not record.get("promotion_rationale"):
257 problems.append(
258 f"{identifier}: proposes '{proposed}' but gives no promotion_rationale. "
259 f"Say what the owner is being asked to confirm"
260 )
261
262 if status in NEEDS_PEAK:
263 peak = record.get("peak_status")
264 if peak not in LADDER:
265 problems.append(
266 f"{identifier}: status '{status}' requires peak_status naming the highest "
267 f"rung it reached, one of {list(LADDER)}"
268 )
269 elif record.get("peak_status") is not None:
270 problems.append(
271 f"{identifier}: peak_status applies only to {list(NEEDS_PEAK)}, not '{status}'"
272 )
273
274 obligations = record.get("obligations") or {}
275 owed = required_obligations(record)
276 for key in owed:
277 if key not in obligations:
278 problems.append(
279 f"{identifier}: status '{status}' owes '{key}', which is not declared"
280 )
281 else:
282 problems.extend(
283 check_satisfaction(identifier, key, obligations[key], pages, published,
284 visibility == "public")
285 )
286 for key in sorted(set(obligations) - set(owed)):
287 problems.append(
288 f"{identifier}: declares obligation '{key}', which status '{status}' does not "
289 f"owe; remove it or claim the status that owes it"
290 )
291
292 concerns = record.get("concerns") or {}
293 for key in sorted(concerns):
294 if key not in VALID_CONCERNS:
295 problems.append(
296 f"{identifier}: concern '{key}' is not in the concern vocabulary of "
297 f"64_Documentation_Extension_Framework"
298 )
299 continue
300 problems.extend(
301 check_satisfaction(identifier, key, concerns[key], pages, published,
302 visibility == "public")
303 )
304
305 # A planned subsystem must not already be presented as working behavior.
306 for family_id in record.get("capability_families", []):
307 family = families.get(family_id)
308 if family is None:
309 problems.append(
310 f"{identifier}: cites capability family '{family_id}', which is not "
311 f"registered"
312 )
313 continue
314 if status != "planned":
315 continue
316 live = sorted(
317 name for name, meta in family.get("value_metadata", {}).items()
318 if (meta or {}).get("status") == "supported"
319 )
320 if live:
321 problems.append(
322 f"{identifier}: is 'planned' but family '{family_id}' already offers "
323 f"supported value(s) {live}; a planned subsystem must not appear as "
324 f"supported behavior"
325 )
326 return problems
327
328
329def main() -> int:
330 """!
331 @brief Report subsystem lifecycle violations.
332 @return Process status code.
333 """
334 document = json.loads(RECORDS.read_text(encoding="utf-8"))
335 records = document["subsystems"]
336 published = set(json.loads(PAGE_TYPES.read_text(encoding="utf-8"))["assignments"])
337 families = {
338 family["id"]: family
339 for family in json.loads(FAMILIES.read_text(encoding="utf-8"))["families"]
340 }
341 problems = validate(records, page_index(), families, published)
342 if problems:
343 print("Subsystem lifecycle violations:", file=sys.stderr)
344 for problem in problems:
345 print(f" {problem}", file=sys.stderr)
346 print(
347 "\nObligations grow with the status a subsystem claims. See\n"
348 "64_Documentation_Extension_Framework section 4. Lower the claimed status, or\n"
349 "write the documentation that status owes - do not add empty prose to pass.",
350 file=sys.stderr,
351 )
352 return 1
353
354 from collections import Counter
355 spread = Counter(record["status"] for record in records)
356 summary = ", ".join(f"{count} {status}" for status, count in sorted(spread.items()))
357 print(f"Subsystem lifecycle audit passed: {len(records)} subsystem(s) ({summary}).")
358 proposals = [r for r in records if r.get("proposed_status")]
359 if proposals:
360 print(f"\n{len(proposals)} promotion(s) awaiting the owner's decision. The "
361 f"documentation meets the higher bar; whether the subsystem has earned "
362 f"the status is a human judgement this gate cannot make:")
363 for record in proposals:
364 print(f" {record['id']}: {record['status']} -> {record['proposed_status']}")
365 print(f" {record['promotion_rationale']}")
366 return 0
367
368
369if __name__ == "__main__":
370 raise SystemExit(main())
dict page_index()
Every documentation page id with the anchors it defines.
list check_satisfaction(str context, str key, spec, dict pages, set published, bool public)
Verify one obligation or concern is genuinely answered.
tuple required_obligations(dict record)
The obligation ids a record owes.
int main()
Report subsystem lifecycle violations.
list validate(list records, dict pages, dict families, set published)
Validate every subsystem record against the lifecycle contract.