PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
 
Loading...
Searching...
No Matches
scaffold_documentation.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Generate documentation skeletons that satisfy the repository's own contracts."""
3
4from __future__ import annotations
5
6import argparse
7import json
8import re
9import sys
10from pathlib import Path
11
12
13REPO_ROOT = Path(__file__).resolve().parents[2]
14PAGES_DIR = REPO_ROOT / "docs" / "pages"
15FAMILIES_PATH = REPO_ROOT / "tests" / "tooling" / "capability_families.json"
16
17PAGE_TYPES = {
18 "tutorial": "Tutorial",
19 "how-to": "How-to",
20 "reference": "Reference",
21 "explanation": "Explanation",
22 "hub": "Hub",
23}
24
25
26# Every scaffold carries this until an author removes it. Structural correctness and
27# completion are separate conditions: a skeleton is deliberately shaped to satisfy the
28# structural audits, so without an explicit marker an untouched scaffold could be
29# published as if it were finished.
30INCOMPLETE_MARKER = "@par SCAFFOLD-INCOMPLETE"
31INCOMPLETE_NOTE = (
32 f"{INCOMPLETE_MARKER}\n"
33 "This was generated from a scaffold and is not finished. Replace every TODO, then\n"
34 "delete this block. Publication gates reject the marker, so an untouched scaffold\n"
35 "cannot ship."
36)
37
38
39def identifier_slug(value: str) -> str:
40 """!
41 @brief Build a safe Doxygen/file identifier from a free-form title.
42
43 @details Titles may contain characters that are illegal in a Doxygen identifier or
44 that would imply a nested path in a filename. The display title is kept
45 intact; only the identifier is sanitized.
46 @param[in] value Free-form title.
47 @return Safe identifier.
48 @throws SystemExit when nothing usable remains.
49 """
50 cleaned = re.sub(r"[^A-Za-z0-9]+", "_", value).strip("_")
51 cleaned = re.sub(r"_{2,}", "_", cleaned)
52 if not cleaned:
53 raise SystemExit(
54 f"Title {value!r} contains no characters usable in an identifier; "
55 f"choose a title with letters or digits."
56 )
57 if cleaned[0].isdigit():
58 cleaned = f"p_{cleaned}"
59 return cleaned
60
61
62def slug(value: str) -> str:
63 """!
64 @brief Anchor slug for a selector value.
65 @param[in] value Public selector value.
66 @return Lowercase underscore slug.
67 """
68 return re.sub(r"[^a-z0-9]+", "_", value.lower()).strip("_")
69
70
71def next_page_number() -> int:
72 """!
73 @brief Lowest unused page number in docs/pages.
74 @return Next available page number.
75 """
76 used = {
77 int(match.group(1))
78 for path in PAGES_DIR.glob("*.md")
79 if (match := re.match(r"(\d+)_", path.name))
80 }
81 return max(used) + 1 if used else 1
82
83
84def capability_entry(family_id: str, value: str) -> str:
85 """!
86 @brief Render a Tier-2 capability entry carrying every required contract part.
87 @param[in] family_id Capability family identifier.
88 @param[in] value Public selector value.
89 @return Markdown text for the entry.
90 """
91 families = {f["id"]: f for f in json.loads(FAMILIES_PATH.read_text(encoding="utf-8"))["families"]}
92 family = families.get(family_id)
93 if family is None:
94 raise SystemExit(
95 f"Unknown family '{family_id}'. Known: {', '.join(sorted(families))}"
96 )
97 anchor = family["entry_anchor_prefix"] + slug(value)
98 return f"""@subsection {anchor}_sub {value}
99
100@anchor {anchor}
101
102{INCOMPLETE_NOTE}
103
104**Identity.** `{family['selector']}` = `{value}` -> TODO: generated flag -> TODO: C enum
105-> @ref TODO_implementing_function.
106
107**What it does.** TODO: the behavior, in a short paragraph.
108
109**When to choose it.** TODO: compare explicitly against its siblings in this family.
110An entry that does not compare is a description, not guidance.
111
112**Parameters it owns.** TODO: options exclusive to this value, required and optional
113separated. Options shared across the family belong on the family page.
114
115**Interactions.** TODO: what it requires, conflicts with, or implies, including
116anything enforced by validation.
117
118**Diagnostics.** TODO: what it logs, and how a reader tells it is working. Name the
119failure signature, not only the success one.
120
121**Evidence.** TODO: facets with sources, or state "Implemented only." Record the same
122sources in `value_metadata["{value}"]["evidence"]` in
123`tests/tooling/capability_families.json` - the audit checks they match.
124
125**Limitations.** TODO: what it cannot do.
126"""
127
128
129def alias_stub(family_id: str, value: str, canonical: str) -> str:
130 """!
131 @brief Render a deprecated-alias stub, which owes three parts rather than eight.
132 @param[in] family_id Capability family identifier.
133 @param[in] value Deprecated selector value.
134 @param[in] canonical Canonical value it resolves to.
135 @return Markdown text for the stub.
136 """
137 families = {f["id"]: f for f in json.loads(FAMILIES_PATH.read_text(encoding="utf-8"))["families"]}
138 family = families.get(family_id)
139 if family is None:
140 raise SystemExit(f"Unknown family '{family_id}'. Known: {', '.join(sorted(families))}")
141 known = set(family.get("value_metadata", {}))
142 if known and canonical not in known:
143 raise SystemExit(
144 f"Canonical target '{canonical}' is not a declared value of '{family_id}'. "
145 f"Known: {', '.join(sorted(known))}"
146 )
147 anchor = family["entry_anchor_prefix"] + slug(value)
148 return f"""@subsection {anchor}_sub {value} (deprecated)
149
150@anchor {anchor}
151
152{INCOMPLETE_NOTE}
153
154**Identity.** `{value}` - a **deprecated alias** that normalizes to `{canonical}`.
155
156**Status.** Deprecated. TODO: why it still parses.
157
158**Migration.** TODO: the exact edits to move to `{canonical}`.
159"""
160
161
162PAGE_SECTIONS = {
163 "tutorial": [
164 ("Outcome", "TODO: what the reader will have produced by the end. State it concretely."),
165 ("Prerequisites", "TODO: software, build state, inputs, and the starting directory."),
166 ("Steps", "TODO: the ordered path. Every command must run as written, in this order."),
167 ("Confirm It Worked", "TODO: the exact files, log lines, or values that mean success."),
168 ("If It Failed", "TODO: the most common failure for this path, and its fix."),
169 ("Next", "TODO: where the reader should go now."),
170 ],
171 "how-to": [
172 ("Goal", "TODO: the one task this page completes."),
173 ("Before You Start", "TODO: what must already be true. Assume the reader can run PICurv."),
174 ("Procedure", "TODO: the steps. Branch only where the situation genuinely differs."),
175 ("Verify", "TODO: how the reader confirms the task is done."),
176 ],
177 "reference": [
178 ("Scope", "TODO: exactly what this page covers, and what it does not."),
179 ("Reference", "TODO: the structured inventory, schema, or option table. Prefer a generated\nfragment over a hand-maintained one."),
180 ("Constraints", "TODO: validation rules, mutual exclusions, and required combinations."),
181 ("Related Surfaces", "TODO: the other pages that own adjacent contracts."),
182 ],
183 "explanation": [
184 ("Concepts", "TODO: the ideas a reader needs before the rationale makes sense."),
185 ("Why It Works This Way", "TODO: the reasoning behind the current design."),
186 ("Alternatives Considered", "TODO: what else was possible, and why it was not chosen."),
187 ("Limitations", "TODO: what this design cannot do."),
188 ],
189 "hub": [
190 ("Routes", "TODO: the pages this hub owns, adopted with @subpage. Routing only - a hub\nthat restates reference prose has become a duplicate of what it routes to."),
191 ],
192}
193
194
195def suggested_filename(title: str, number: int) -> str:
196 """!
197 @brief Filename for a scaffolded page, using the same sanitized identifier as the page ID.
198 @param[in] title Free-form page title.
199 @param[in] number Page number.
200 @return Safe filename.
201 """
202 return f"{number:02d}_{identifier_slug(title)}.md"
203
204
205def page(kind: str, title: str, number: int) -> str:
206 """!
207 @brief Render a page skeleton shaped to its document type's contract.
208
209 @details Each type gets the sections it actually owes, not a generic outline. A
210 tutorial that does not state its outcome, or a reference that does not
211 declare its scope, is not the type it claims to be.
212 @param[in] kind Page type key.
213 @param[in] title Human-readable page title.
214 @param[in] number Page number.
215 @return Markdown text for the page.
216 """
217 safe = identifier_slug(title)
218 identifier = f"{number:02d}_{safe}"
219 owes = {
220 "tutorial": "a working outcome: every command runs as written, from a stated starting state",
221 "how-to": "a completed task, assuming the reader can already run PICurv",
222 "reference": "accuracy and completeness within its declared scope",
223 "explanation": "understanding: why it is like this, and what the alternatives were",
224 "hub": "complete and current routing, and nothing else",
225 }[kind]
226
227 body = [
228 f"@page {identifier} {title}",
229 "",
230 f"@anchor _{safe}",
231 "",
232 f"@pagemeta{{{PAGE_TYPES[kind]}, TODO audience, TODO status}}",
233 "",
234 INCOMPLETE_NOTE,
235 "",
236 "TODO: one paragraph saying what this page is for.",
237 "",
238 f"This page owes the reader {owes}. See **@subpage 63_Page_Type_Contract** for what a",
239 f"{PAGE_TYPES[kind]} must refuse to do.",
240 "",
241 "@tableofcontents",
242 "",
243 ]
244 for index, (heading, guidance) in enumerate(PAGE_SECTIONS[kind], start=1):
245 section = identifier_slug(heading).lower()
246 body += [f"@section p{number}_{section}_sec {index}. {heading}", "", guidance, ""]
247 last = len(PAGE_SECTIONS[kind]) + 1
248 body += [
249 f"@section p{number}_related_sec {last}. Related Documentation",
250 "",
251 "- **@subpage 47_Documentation_Catalog**",
252 "",
253 ]
254 return "\n".join(body)
255
256
257def subsystem_record(identifier: str, title: str) -> str:
258 """!
259 @brief Render the machine-readable lifecycle record a `planned` subsystem owes.
260
261 @details The charter is prose; this is the record `make audit-subsystems` reads.
262 A planned subsystem owes only four obligations, so the stub is short by
263 design - the gate grows with the status, not with the scaffold.
264 @param[in] identifier Subsystem id.
265 @param[in] title Human-readable subsystem name.
266 @return JSON text to paste into tests/tooling/subsystem_records.json.
267 """
268 record = {
269 "id": identifier,
270 "title": title,
271 "status": "planned",
272 "visibility": "public",
273 "previous_status": None,
274 "capability_families": [],
275 "obligations": {
276 "purpose": {"page": "TODO_page_id", "anchor": "TODO_anchor"},
277 "intended_scope": {"page": "TODO_page_id", "anchor": "TODO_anchor"},
278 "design_owner": {"value": "TODO: name"},
279 "not_implemented_status": {"page": "TODO_page_id", "anchor": "TODO_anchor"},
280 },
281 "concerns": {},
282 }
283 return json.dumps(record, indent=2)
284
285
286def subsystem_charter(identifier: str, title: str) -> str:
287 """!
288 @brief Render a subsystem charter at the `planned` lifecycle stage.
289
290 @details Requirements grow with the status a subsystem claims, so this scaffold
291 asks only what `planned` owes. The later stages are listed so the author
292 can see what each promotion will require.
293 @param[in] identifier Subsystem identifier.
294 @param[in] title Human-readable subsystem name.
295 @return Markdown text for the charter.
296 """
297 return f"""# Subsystem charter: {title}
298
299{INCOMPLETE_MARKER} - replace every TODO, then delete this line.
300
301Identifier: `{identifier}`
302Lifecycle status: **planned**
303
304## Purpose
305
306TODO: what problem this solves, and for whom.
307
308## Intended scope
309
310TODO: what it will cover.
311
312## Explicitly out of scope
313
314TODO: what it will not cover. This is the most useful section to write early.
315
316## Design owner
317
318TODO: name.
319
320## Not implemented
321
322This subsystem is `planned`. Nothing here describes working behavior, and it must not
323appear in user-facing reference until it reaches `public experimental`.
324
325## Concern modules
326
327Declare which apply, per **64_Documentation_Extension_Framework**. A concern may be
328marked not applicable **with a stated reason**; an empty heading is not an answer.
329
330- [ ] Numerical method
331- [ ] User selector
332- [ ] Units and nondimensionalization
333- [ ] Artifact topology and storage lifecycle
334- [ ] Persistent/restart state
335- [ ] Determinism and reproducibility
336- [ ] MPI/distributed execution
337- [ ] External service
338- [ ] Security/credentials
339- [ ] Generated artifact
340- [ ] File format
341- [ ] Concurrency, permissions, destructive scope
342- [ ] Backward compatibility
343- [ ] Scientific verification and validation
344
345## What each promotion will require
346
347| To reach | Add |
348|---|---|
349| `internal` | Scope boundary, architecture boundary, dependencies, developer entry points |
350| `public experimental` | Configuration, selection guidance, observability, limitations, safe-use boundaries |
351| `supported` | Evidence, lifecycle and restart behavior, operations, troubleshooting, examples, complete reference |
352"""
353
354
355def main() -> int:
356 """!
357 @brief Emit a documentation skeleton for the requested kind.
358 @return Process status code.
359 """
360 parser = argparse.ArgumentParser(
361 description="Generate documentation skeletons that satisfy the repository's contracts.",
362 epilog=(
363 "Examples:\n"
364 " scaffold_documentation.py capability --family boundary.handler --value slip_wall\n"
365 " scaffold_documentation.py alias --family momentum.solver --value 'Old Name' "
366 "--canonical 'New Name'\n"
367 " scaffold_documentation.py page --type how-to --title 'Running On A Cluster'\n"
368 " scaffold_documentation.py subsystem --id ibm --title 'Immersed Boundaries'\n"
369 ),
370 formatter_class=argparse.RawTextHelpFormatter,
371 )
372 sub = parser.add_subparsers(dest="kind", required=True)
373
374 p_cap = sub.add_parser("capability", help="A Tier-2 entry for a new selector value.")
375 p_cap.add_argument("--family", required=True)
376 p_cap.add_argument("--value", required=True)
377
378 p_alias = sub.add_parser("alias", help="A deprecated-alias stub.")
379 p_alias.add_argument("--family", required=True)
380 p_alias.add_argument("--value", required=True)
381 p_alias.add_argument("--canonical", required=True)
382
383 p_page = sub.add_parser("page", help="A new documentation page of a declared type.")
384 p_page.add_argument("--type", required=True, choices=sorted(PAGE_TYPES))
385 p_page.add_argument("--title", required=True)
386
387 p_sub = sub.add_parser("subsystem", help="A subsystem charter at the planned stage.")
388 p_sub.add_argument("--id", required=True)
389 p_sub.add_argument("--title", required=True)
390
391 args = parser.parse_args()
392
393 if args.kind == "capability":
394 print(capability_entry(args.family, args.value))
395 print("# Next: paste into the family page, then run 'make audit-capability'.", file=sys.stderr)
396 elif args.kind == "alias":
397 print(alias_stub(args.family, args.value, args.canonical))
398 print("# Next: declare alias_of in capability_families.json.", file=sys.stderr)
399 elif args.kind == "page":
400 number = next_page_number()
401 print(page(args.type, args.title, number))
402 print(
403 f"# Next: save as docs/pages/{suggested_filename(args.title, number)}, "
404 f"then adopt it with @subpage from a hub page or it will be reported as orphaned.",
405 file=sys.stderr,
406 )
407 else:
408 print(subsystem_charter(args.id, args.title))
409 print("\n<!-- Paste into the \"subsystems\" list of "
410 "tests/tooling/subsystem_records.json:\n")
411 print(subsystem_record(args.id, args.title))
412 print("-->")
413 print("# Next: add the record above to tests/tooling/subsystem_records.json and run\n"
414 "# make audit-subsystems. Record it in the contract registry too if it\n"
415 "# introduces invariants.", file=sys.stderr)
416 return 0
417
418
419if __name__ == "__main__":
420 raise SystemExit(main())
str slug(str value)
Anchor slug for a selector value.
int main()
Emit a documentation skeleton for the requested kind.
str subsystem_charter(str identifier, str title)
Render a subsystem charter at the planned lifecycle stage.
str suggested_filename(str title, int number)
Filename for a scaffolded page, using the same sanitized identifier as the page ID.
int next_page_number()
Lowest unused page number in docs/pages.
str capability_entry(str family_id, str value)
Render a Tier-2 capability entry carrying every required contract part.
str subsystem_record(str identifier, str title)
Render the machine-readable lifecycle record a planned subsystem owes.
str identifier_slug(str value)
Build a safe Doxygen/file identifier from a free-form title.
str page(str kind, str title, int number)
Render a page skeleton shaped to its document type's contract.
str alias_stub(str family_id, str value, str canonical)
Render a deprecated-alias stub, which owes three parts rather than eight.