2"""Generate documentation skeletons that satisfy the repository's own contracts."""
4from __future__
import annotations
10from pathlib
import Path
13REPO_ROOT = Path(__file__).resolve().parents[2]
14PAGES_DIR = REPO_ROOT /
"docs" /
"pages"
15FAMILIES_PATH = REPO_ROOT /
"tests" /
"tooling" /
"capability_families.json"
18 "tutorial":
"Tutorial",
20 "reference":
"Reference",
21 "explanation":
"Explanation",
30INCOMPLETE_MARKER =
"@par SCAFFOLD-INCOMPLETE"
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"
41 @brief Build a safe Doxygen/file identifier from a free-form title.
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.
50 cleaned = re.sub(
r"[^A-Za-z0-9]+",
"_", value).strip(
"_")
51 cleaned = re.sub(
r"_{2,}",
"_", cleaned)
54 f
"Title {value!r} contains no characters usable in an identifier; "
55 f
"choose a title with letters or digits."
57 if cleaned[0].isdigit():
58 cleaned = f
"p_{cleaned}"
62def slug(value: str) -> str:
64 @brief Anchor slug for a selector value.
65 @param[in] value Public selector value.
66 @return Lowercase underscore slug.
68 return re.sub(
r"[^a-z0-9]+",
"_", value.lower()).strip(
"_")
73 @brief Lowest unused page number in docs/pages.
74 @return Next available page number.
78 for path
in PAGES_DIR.glob(
"*.md")
79 if (match := re.match(
r"(\d+)_", path.name))
81 return max(used) + 1
if used
else 1
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.
91 families = {f[
"id"]: f
for f
in json.loads(FAMILIES_PATH.read_text(encoding=
"utf-8"))[
"families"]}
92 family = families.get(family_id)
95 f
"Unknown family '{family_id}'. Known: {', '.join(sorted(families))}"
97 anchor = family[
"entry_anchor_prefix"] +
slug(value)
98 return f
"""@subsection {anchor}_sub {value}
104**Identity.** `{family['selector']}` = `{value}` -> TODO: generated flag -> TODO: C enum
105-> @ref TODO_implementing_function.
107**What it does.** TODO: the behavior, in a short paragraph.
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.
112**Parameters it owns.** TODO: options exclusive to this value, required and optional
113separated. Options shared across the family belong on the family page.
115**Interactions.** TODO: what it requires, conflicts with, or implies, including
116anything enforced by validation.
118**Diagnostics.** TODO: what it logs, and how a reader tells it is working. Name the
119failure signature, not only the success one.
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.
125**Limitations.** TODO: what it cannot do.
129def alias_stub(family_id: str, value: str, canonical: str) -> str:
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.
137 families = {f[
"id"]: f
for f
in json.loads(FAMILIES_PATH.read_text(encoding=
"utf-8"))[
"families"]}
138 family = families.get(family_id)
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:
144 f
"Canonical target '{canonical}' is not a declared value of '{family_id}'. "
145 f
"Known: {', '.join(sorted(known))}"
147 anchor = family[
"entry_anchor_prefix"] +
slug(value)
148 return f
"""@subsection {anchor}_sub {value} (deprecated)
154**Identity.** `{value}` - a **deprecated alias** that normalizes to `{canonical}`.
156**Status.** Deprecated. TODO: why it still parses.
158**Migration.** TODO: the exact edits to move to `{canonical}`.
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."),
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."),
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."),
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."),
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."),
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.
202 return f
"{number:02d}_{identifier_slug(title)}.md"
205def page(kind: str, title: str, number: int) -> str:
207 @brief Render a page skeleton shaped to its document type's contract.
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.
218 identifier = f
"{number:02d}_{safe}"
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",
228 f
"@page {identifier} {title}",
232 f
"@pagemeta{{{PAGE_TYPES[kind]}, TODO audience, TODO status}}",
236 "TODO: one paragraph saying what this page is for.",
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.",
244 for index, (heading, guidance)
in enumerate(PAGE_SECTIONS[kind], start=1):
246 body += [f
"@section p{number}_{section}_sec {index}. {heading}",
"", guidance,
""]
247 last = len(PAGE_SECTIONS[kind]) + 1
249 f
"@section p{number}_related_sec {last}. Related Documentation",
251 "- **@subpage 47_Documentation_Catalog**",
254 return "\n".join(body)
259 @brief Render the machine-readable lifecycle record a `planned` subsystem owes.
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.
272 "visibility":
"public",
273 "previous_status":
None,
274 "capability_families": [],
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"},
283 return json.dumps(record, indent=2)
288 @brief Render a subsystem charter at the `planned` lifecycle stage.
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.
297 return f
"""# Subsystem charter: {title}
299{INCOMPLETE_MARKER} - replace every TODO, then delete this line.
301Identifier: `{identifier}`
302Lifecycle status: **planned**
306TODO: what problem this solves, and for whom.
310TODO: what it will cover.
312## Explicitly out of scope
314TODO: what it will not cover. This is the most useful section to write early.
322This subsystem is `planned`. Nothing here describes working behavior, and it must not
323appear in user-facing reference until it reaches `public experimental`.
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.
330- [ ] Numerical method
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
341- [ ] Concurrency, permissions, destructive scope
342- [ ] Backward compatibility
343- [ ] Scientific verification and validation
345## What each promotion will require
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 |
357 @brief Emit a documentation skeleton for the requested kind.
358 @return Process status code.
360 parser = argparse.ArgumentParser(
361 description=
"Generate documentation skeletons that satisfy the repository's contracts.",
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"
370 formatter_class=argparse.RawTextHelpFormatter,
372 sub = parser.add_subparsers(dest=
"kind", required=
True)
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)
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)
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)
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)
391 args = parser.parse_args()
393 if args.kind ==
"capability":
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":
401 print(
page(args.type, args.title, number))
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.",
409 print(
"\n<!-- Paste into the \"subsystems\" list of "
410 "tests/tooling/subsystem_records.json:\n")
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)
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.