2"""Extract the public capability inventory from executable sources and render it for the docs."""
4from __future__
import annotations
11from pathlib
import Path
14REPO_ROOT = Path(__file__).resolve().parents[2]
15REGISTRY_PATH = REPO_ROOT /
"tests" /
"tooling" /
"capability_families.json"
16GENERATED_DIR = REPO_ROOT /
"docs" /
"generated"
21 @brief Load the capability family registry.
22 @return Parsed registry mapping.
24 return json.loads(REGISTRY_PATH.read_text(encoding=
"utf-8"))
29 @brief Evaluate a literal AST node, additionally accepting the bare `set()` call
30 that appears in the boundary-handler specs for empty parameter sets.
31 @param[in] node Parsed AST node.
32 @return The Python value the node denotes.
34 if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id ==
"set":
35 return set(
literal(node.args[0]))
if node.args
else set()
36 if isinstance(node, ast.Dict):
37 return {
literal(k):
literal(v)
for k, v
in zip(node.keys, node.values)}
38 if isinstance(node, ast.Set):
39 return {
literal(e)
for e
in node.elts}
40 if isinstance(node, (ast.List, ast.Tuple)):
41 return [
literal(e)
for e
in node.elts]
42 return ast.literal_eval(node)
47 @brief Parse a dotted module into one syntax tree, whether file or package.
49 @details A public surface may be a package: `picurv_cli.storage` exposes its
50 constants through its `__init__`, but they are defined across its
51 modules. Concatenating their bodies lets the readers below stay written
52 against a single tree, which is what they mean by "the module".
53 @param[in] module Dotted module name.
54 @return Syntax tree covering every file the module resolves to.
56 relative = Path(*module.split(
"."))
57 package = REPO_ROOT / relative
59 combined = ast.Module(body=[], type_ignores=[])
60 for child
in sorted(package.glob(
"*.py")):
61 if child.name ==
"__init__.py":
63 combined.body.extend(ast.parse(child.read_text(encoding=
"utf-8")).body)
65 return ast.parse((REPO_ROOT / relative.with_suffix(
".py")).read_text(encoding=
"utf-8"))
70 @brief Read a public-surface dictionary from the CLI package without importing PETSc.
71 @param[in] module Dotted module name.
72 @param[in] symbol Module-level dictionary name.
73 @return Mapping of selector value to its declared parameter contract.
76 for node
in tree.body:
77 if not isinstance(node, ast.Assign):
79 targets = [t.id
for t
in node.targets
if isinstance(t, ast.Name)]
80 if symbol
not in targets:
83 result: dict[str, dict] = {}
84 for key, value
in raw.items():
85 if isinstance(value, dict):
87 "types": sorted(value.get(
"types", [])),
88 "required_params": sorted(value.get(
"required_params", [])),
89 "optional_params": sorted(value.get(
"optional_params", [])),
92 result[key] = {
"maps_to": value}
94 raise RuntimeError(f
"{symbol} not found in {module}")
99 @brief Read the canonical selector strings accepted by a normalizer function.
100 @param[in] module Dotted module name.
101 @param[in] symbol Normalizer function name.
102 @return Mapping of canonical value to the runtime token it maps to.
105 for node
in ast.walk(tree):
106 if isinstance(node, ast.FunctionDef)
and node.name == symbol:
107 for inner
in ast.walk(node):
108 if isinstance(inner, ast.Dict)
and inner.keys:
110 mapping = ast.literal_eval(inner)
113 if all(isinstance(k, str)
for k
in mapping):
114 return {k: {
"maps_to": v}
for k, v
in mapping.items()}
115 raise RuntimeError(f
"no canonical mapping found in {module}.{symbol}")
120 @brief Return the source text of one C function, so extraction never spans the file.
121 @param[in] path Repository-relative C source path.
122 @param[in] function Function name to isolate.
123 @return Source text of that function body.
124 @throws RuntimeError when the function cannot be located.
126 text = (REPO_ROOT / path).read_text(encoding=
"utf-8")
127 match = re.search(rf
"^[A-Za-z_][\w \*]*\b{re.escape(function)}\s*\(", text, re.M)
129 raise RuntimeError(f
"function {function} not found in {path}")
130 index = text.index(
"{", match.end() - 1)
132 for offset
in range(index, len(text)):
133 if text[offset] ==
"{":
135 elif text[offset] ==
"}":
138 return text[index : offset + 1]
139 raise RuntimeError(f
"unbalanced braces while reading {function} in {path}")
144 @brief Read the accepted values from a normalizer that validates by set membership.
146 @details Some normalizers check `if value not in {...}` rather than mapping through a
147 dict. The accepted set is still the public surface, so it is extracted the
148 same way rather than being hand-listed.
149 @param[in] module Dotted module name.
150 @param[in] symbol Normalizer function name.
151 @return Mapping of accepted value to the token it resolves to.
154 for node
in ast.walk(tree):
155 if not (isinstance(node, ast.FunctionDef)
and node.name == symbol):
158 for inner
in ast.walk(node):
159 if isinstance(inner, ast.Set):
161 members = {
literal(element)
for element
in inner.elts}
162 except (ValueError, TypeError):
164 if all(isinstance(member, str)
for member
in members):
165 for member
in members:
166 collected.setdefault(member, {
"maps_to": member})
169 raise RuntimeError(f
"no membership set found in {module}.{symbol}")
174 @brief Read accepted values from a normalizer that compares against string literals.
176 @details A third normalizer shape: `if normalized == "ucat": ... elif ... == "ucont"`.
177 The compared literals are the public surface, so they are extracted rather
178 than hand-listed, keeping the inventory tied to the code.
179 @param[in] module Dotted module name.
180 @param[in] symbol Normalizer function name.
181 @return Mapping of accepted value to itself.
184 for node
in ast.walk(tree):
185 if not (isinstance(node, ast.FunctionDef)
and node.name == symbol):
188 for inner
in ast.walk(node):
189 if not isinstance(inner, ast.Compare):
191 if not any(isinstance(op, (ast.Eq, ast.NotEq))
for op
in inner.ops):
193 for comparator
in inner.comparators:
194 if isinstance(comparator, ast.Constant)
and isinstance(comparator.value, str):
196 collected.setdefault(comparator.value, {
"maps_to": comparator.value})
199 raise RuntimeError(f
"no equality chain found in {module}.{symbol}")
204 @brief Extract the case-insensitive selector strings a C parser accepts.
205 @param[in] path Repository-relative C source path.
206 @param[in] function Parser function name.
207 @return Mapping of accepted selector string to the enum constant it selects.
211 r'strcasecmp\(\s*str\s*,\s*"([^"]+)"\s*\)\s*==\s*0\s*\)\s*\*handler_out\s*=\s*([A-Z][A-Z0-9_]+)',
219 @brief Extract an exact-match token chain that assigns an enum to a context field.
221 Handles the `strcmp(buf, "TOKEN") == 0 ... field = ENUM;` shape used for
222 generated PETSc option tokens, including chains where several tokens share one
223 assignment (an alias arm).
224 @param[in] path Repository-relative C source path.
225 @param[in] function Enclosing function name.
226 @param[in] variable Name of the char buffer holding the option value.
227 @param[in] field Assigned context field, for example `mom_solver_type`.
228 @return Mapping of accepted token to the enum constant it selects.
231 mapping: dict[str, str] = {}
232 pattern = re.compile(
233 r'((?:strcmp\(\s*' + re.escape(variable) +
r'\s*,\s*"[^"]+"\s*\)\s*==\s*0\s*\|?\|?\s*)+)'
234 r"[^{]*\{[^}]*?\b" + re.escape(field) +
r"\s*=\s*([A-Z][A-Z0-9_]+)\s*;",
237 for arm, enum
in pattern.findall(body):
238 for token
in re.findall(
r'"([^"]+)"', arm):
239 mapping[token] = enum
245 @brief Extract the enum constants a C factory switch dispatches on.
246 @param[in] path Repository-relative C source path.
247 @param[in] function Enclosing function name.
248 @param[in] prefix Enum constant prefix.
249 @return Set of dispatched enum constants.
252 return set(re.findall(rf
"case\s+({re.escape(prefix)}[A-Z0-9_]+)\s*:", body))
257 @brief Extract the enum constants an if/else dispatch chain compares against.
258 @param[in] path Repository-relative C source path.
259 @param[in] function Enclosing function name.
260 @param[in] prefix Enum constant prefix.
261 @return Set of enum constants the dispatch acts on.
264 return set(re.findall(rf
"==\s*({re.escape(prefix)}[A-Z0-9_]+)", body))
269 @brief Extract the members of a C enum.
270 @param[in] path Repository-relative header path.
271 @param[in] symbol Enum type name.
272 @return Set of enum member names.
274 text = (REPO_ROOT / path).read_text(encoding=
"utf-8")
275 match = re.search(
r"typedef\s+enum\s*\{([^{}]*)\}\s*" + re.escape(symbol) +
r"\s*;", text, re.S)
277 raise RuntimeError(f
"enum {symbol} not found in {path}")
282 body = re.sub(
r"/\*.*?\*/",
"", match.group(1), flags=re.S)
283 body = re.sub(
r"//[^\n]*",
"", body)
284 for chunk
in body.split(
","):
285 name = re.match(
r"\s*([A-Z][A-Z0-9_]*)\s*(?:=|$)", chunk)
287 members.add(name.group(1))
293 @brief Read the accepted values from a named module-level choice set.
295 @details The preferred shape. A choice set written as an inline literal at its point
296 of use is invisible to the census, so the rule is that it must be a named
297 module-level constant - a tuple, list, set, or dict of strings. A dict maps
298 each accepted spelling to what it resolves to; a sequence maps each value
300 @param[in] module Dotted module name.
301 @param[in] symbol Constant name.
302 @return Mapping of accepted value to the token it resolves to.
305 for node
in tree.body:
306 if not isinstance(node, ast.Assign):
308 if not any(isinstance(target, ast.Name)
and target.id == symbol
309 for target
in node.targets):
312 if isinstance(value, dict):
313 if not all(isinstance(k, str)
for k
in value):
314 raise RuntimeError(f
"{module}.{symbol} is not keyed by strings")
318 if all(isinstance(v, str)
for v
in value.values()):
319 return {k: {
"maps_to": v}
for k, v
in value.items()}
320 return {k: {
"maps_to": k}
for k
in value}
321 if isinstance(value, (tuple, list, set, frozenset)):
322 members =
list(value)
323 if not all(isinstance(member, str)
for member
in members):
324 raise RuntimeError(f
"{module}.{symbol} is not a sequence of strings")
325 return {member: {
"maps_to": member}
for member
in members}
326 raise RuntimeError(f
"{module}.{symbol} is not a choice set")
327 raise RuntimeError(f
"no module-level constant {symbol} in {module}")
332 @brief Build one family's inventory record from its declared sources.
333 @param[in] family Family registry entry.
334 @return Inventory record for the family.
336 surface = family[
"public_surface"]
337 if surface[
"kind"] ==
"python_dict":
339 elif surface[
"kind"] ==
"python_normalizer":
341 elif surface[
"kind"] ==
"python_membership":
343 elif surface[
"kind"] ==
"python_equality_chain":
345 elif surface[
"kind"] ==
"python_constant":
348 raise RuntimeError(f
"unknown public_surface kind: {surface['kind']}")
351 for source
in family.get(
"parity_sources", []):
352 kind = source[
"kind"]
353 if kind ==
"c_string_map":
355 elif kind ==
"c_token_map":
357 source[
"path"], source[
"function"], source[
"variable"], source[
"field"]
359 elif kind ==
"c_switch":
360 found =
c_switch_values(source[
"path"], source[
"function"], source[
"prefix"])
361 elif kind ==
"c_dispatch":
363 elif kind ==
"c_enum":
366 raise RuntimeError(f
"unknown parity source kind: {kind}")
367 if isinstance(found, dict):
368 parity.append({
"source": source,
"values": sorted(found),
"mapping": found})
370 parity.append({
"source": source,
"values": sorted(found)})
374 "title": family[
"title"],
375 "selector": family[
"selector"],
376 "family_page": family[
"family_page"],
377 "public_values": values,
384 @brief Merge declared per-value metadata (status, alias target) into the inventory.
385 @param[in,out] inventory Collected family records.
386 @param[in] registry Parsed registry mapping.
389 entries = {entry[
"id"]: entry
for entry
in registry[
"families"]}
390 for family
in inventory:
391 declared = entries[family[
"id"]].get(
"value_metadata", {})
392 for name, spec
in family[
"public_values"].items():
393 meta = declared.get(name, {})
394 if meta.get(
"alias_of"):
395 spec[
"alias_of"] = meta[
"alias_of"]
396 if meta.get(
"spelling_of"):
397 spec[
"spelling_of"] = meta[
"spelling_of"]
402 spelling_target = meta.get(
"spelling_of")
404 spec[
"status"] = declared.get(spelling_target, {}).get(
"status",
"supported")
405 elif meta.get(
"status"):
406 spec[
"status"] = meta[
"status"]
409def classify(inventory: list[dict]) -> dict[str, int]:
411 @brief Count selectable, alias, and latent values separately.
413 A single total conflates three different things: what a user can choose, what is
414 only kept readable for old configs, and what is declared but unreachable.
415 @param[in] inventory Collected family records.
416 @return Mapping of category to count.
418 counts = {
"selectable": 0,
"spelling": 0,
"alias": 0,
"latent": 0}
419 for family
in inventory:
420 for spec
in family[
"public_values"].values():
421 if spec.get(
"reachability") ==
"latent":
422 counts[
"latent"] += 1
423 elif spec.get(
"alias_of"):
425 elif spec.get(
"spelling_of"):
426 counts[
"spelling"] += 1
428 counts[
"selectable"] += 1
434 @brief Mark declared values that no other family can actually satisfy as latent.
436 A boundary type is only selectable if some public handler accepts it. Listing a
437 type no handler supports advertises a capability every complete configuration
438 would be rejected for.
439 @param[in,out] inventory Collected family records.
440 @param[in] registry Parsed registry mapping.
443 by_id = {family[
"id"]: family
for family
in inventory}
444 for entry
in registry[
"families"]:
445 spec = entry.get(
"reachable_from")
448 provider = by_id.get(spec[
"family"])
449 target = by_id.get(entry[
"id"])
450 if provider
is None or target
is None:
451 raise RuntimeError(f
"reachable_from names an unknown family: {spec['family']}")
452 reachable: set[str] = set()
453 for value
in provider[
"public_values"].values():
454 reachable.update(value.get(spec[
"field"], []))
455 for name, value
in target[
"public_values"].items():
456 resolved = value.get(
"maps_to", name)
460 is_reachable = resolved
in reachable
461 value[
"reachable"] = is_reachable
462 value[
"reachability"] =
"reachable" if is_reachable
else "latent"
467 @brief Anchor name of the Tier-2 entry for one selector value.
468 @param[in] registry_entry Registry entry carrying the anchor prefix.
469 @param[in] value Public selector value.
472 return registry_entry[
"entry_anchor_prefix"] + re.sub(
r"[^a-z0-9]+",
"_", value.lower()).strip(
"_")
477 @brief Escape text for inclusion in generated HTML.
478 @param[in] text Raw text.
479 @return Escaped text.
481 return text.replace(
"&",
"&").replace(
"<",
"<").replace(
">",
">")
486 @brief Values whose Tier-2 entry anchor is actually present on the family page.
488 Generated tables must not link to an entry that does not exist: a deferred or
489 latent value has no anchor, and a dead in-page link is worse than plain text.
490 @param[in] family Collected family record.
491 @param[in] registry_entry Registry entry naming the family page.
492 @return Set of value names that have an entry.
494 page = REPO_ROOT /
"docs" /
"pages" / f
"{registry_entry['family_page']}.md"
495 if not page.is_file():
497 text = page.read_text(encoding=
"utf-8")
498 anchors = set(re.findall(
r"^@anchor\s+([A-Za-z0-9_]+)\s*$", text, re.M))
501 for name
in family[
"public_values"]
508 @brief Render one family's value table as a Doxygen-includable HTML fragment.
510 HTML rather than Markdown because Doxygen's plain include command inserts Markdown
511 verbatim as a code block, while its HTML include command inserts real markup. Each value links to its
512 Tier-2 entry so the inventory is a route into the documentation, not a dead list.
513 @param[in] family Collected family record.
514 @param[in] registry_entry Registry entry for the same family.
515 @return HTML fragment text.
517 values = family[
"public_values"]
519 has_params = any(
"required_params" in v
for v
in values.values())
520 latent_present = any(
521 v.get(
"reachability") ==
"latent"
523 or v.get(
"spelling_of")
524 or v.get(
"status",
"supported") !=
"supported"
525 for v
in values.values()
529 f
"<!-- GENERATED FILE - do not edit by hand. Family: {family['id']}.",
530 " Regenerate with: make docs-inventory -->",
531 '<table class="markdownTable">',
534 headers = [
"Value",
"Applies to",
"Required parameters",
"Optional parameters"]
if has_params \
535 else [
"Value",
"Maps to"]
537 headers.append(
"Status")
538 out += [f
'<th class="markdownTableHeadNone">{h}</th>' for h
in headers]
541 for name, spec
in sorted(values.items()):
543 label = f
"<code>{html_escape(name)}</code>"
547 target = spec.get(
"spelling_of")
or name
548 documented = target
in documented_values
549 if documented
and spec.get(
"reachability") !=
"latent":
550 label = f
'<a href="#{entry_anchor(registry_entry, target)}">{label}</a>'
553 cells.append(
", ".join(f
"<code>{html_escape(x)}</code>" for x
in spec.get(
"types", []))
or "-")
555 ", ".join(f
"<code>{html_escape(x)}</code>" for x
in spec.get(
"required_params", []))
or "none"
558 ", ".join(f
"<code>{html_escape(x)}</code>" for x
in spec.get(
"optional_params", []))
or "none"
561 cells.append(f
"<code>{html_escape(str(spec.get('maps_to', '-')))}</code>")
563 if spec.get(
"reachability") ==
"latent":
564 cells.append(
"<b>latent - not selectable</b>")
565 elif spec.get(
"alias_of"):
567 "<b>deprecated</b> - alias of <code>"
571 elif spec.get(
"spelling_of"):
573 "accepted spelling of <code>"
581 status = spec.get(
"status",
"supported")
583 f
"<b>{html_escape(status)}</b>" if status !=
"supported" else "supported"
585 out.append(
"<tr>" +
"".join(f
'<td class="markdownTableBodyNone">{c}</td>' for c
in cells) +
"</tr>")
587 out.append(
"</table>")
588 return "\n".join(out) +
"\n"
593 @brief Path of the per-family includable fragment.
594 @param[in] family_id Family identifier.
595 @return Fragment path under the generated directory.
597 return GENERATED_DIR / f
"capability_inventory_{family_id.replace('.', '_')}.html"
602 @brief Render the project-wide capability-by-evidence matrix as an HTML fragment.
604 A scientist deciding whether a result is credible needs to see, in one place,
605 what confidence the project claims for each capability. An empty row is a real
606 answer - it says "implemented only".
607 @param[in] inventory Collected family records.
608 @param[in] registry Parsed registry mapping.
609 @return HTML fragment text.
611 facets = registry[
"evidence_facets"]
612 order = [
"unit",
"integration",
"analytical",
"benchmark",
"reference",
"production"]
613 entries = {e[
"id"]: e
for e
in registry[
"families"]}
615 "<!-- GENERATED FILE - do not edit by hand. Regenerate with: make docs-inventory -->",
616 '<table class="markdownTable">',
618 '<th class="markdownTableHeadNone">Capability</th>',
619 '<th class="markdownTableHeadNone">Family</th>',
621 out += [f
'<th class="markdownTableHeadNone">{html_escape(facets[f])}</th>' for f
in order]
623 for family
in inventory:
624 meta = entries[family[
"id"]].get(
"value_metadata", {})
625 for name, spec
in sorted(family[
"public_values"].items()):
626 if spec.get(
"spelling_of")
or spec.get(
"reachability") ==
"latent":
628 have = dict(meta.get(name, {}).get(
"evidence", {})
or {})
629 label = f
"<code>{html_escape(name)}</code>"
632 label = f
'<a href="{family["family_page"]}.html#{anchor}">{label}</a>'
633 cells = [label, f
"<code>{html_escape(family['id'])}</code>"]
638 +
'">✓</span>'
644 out.append(
"<tr>" +
"".join(f
'<td class="markdownTableBodyNone">{c}</td>' for c
in cells) +
"</tr>")
645 out.append(
"</table>")
646 return "\n".join(out) +
"\n"
651 @brief Render the inventory as a Doxygen-includable Markdown fragment.
652 @param[in] inventory Collected family records.
653 @return Markdown text.
656 "<!-- GENERATED FILE - do not edit by hand.",
657 " Regenerate with: make docs-inventory",
658 " Source of truth: the Python validation layer named per family below. -->",
661 for family
in inventory:
662 lines.append(f
"### {family['title']}")
664 lines.append(f
"Selector: `{family['selector']}`")
666 has_params = any(
"required_params" in v
for v
in family[
"public_values"].values())
668 lines.append(
"| Value | Applies to | Required parameters | Optional parameters |")
669 lines.append(
"|---|---|---|---|")
670 for name, spec
in sorted(family[
"public_values"].items()):
671 types =
", ".join(f
"`{t}`" for t
in spec.get(
"types", []))
or "-"
672 req =
", ".join(f
"`{p}`" for p
in spec.get(
"required_params", []))
or "none"
673 opt =
", ".join(f
"`{p}`" for p
in spec.get(
"optional_params", []))
or "none"
674 lines.append(f
"| `{name}` | {types} | {req} | {opt} |")
676 lines.append(
"| Value | Maps to |")
677 lines.append(
"|---|---|")
678 for name, spec
in sorted(family[
"public_values"].items()):
679 lines.append(f
"| `{name}` | `{spec.get('maps_to', '-')}` |")
681 return "\n".join(lines)
686 @brief Generate the capability inventory artifacts.
687 @return Process status code.
689 parser = argparse.ArgumentParser(description=
"Generate the public capability inventory.")
690 parser.add_argument(
"--check", action=
"store_true", help=
"Fail if generated output is stale.")
691 args = parser.parse_args()
694 entries = {f[
"id"]: f
for f
in registry[
"families"]}
695 inventory = [
collect(family)
for family
in registry[
"families"]]
698 snapshot = json.dumps(inventory, indent=2, sort_keys=
True) +
"\n"
701 GENERATED_DIR.mkdir(parents=
True, exist_ok=
True)
702 json_path = GENERATED_DIR /
"capability_inventory.json"
703 md_path = GENERATED_DIR /
"capability_inventory.md"
705 managed = {json_path: snapshot, md_path: markdown}
707 for family
in inventory:
712 owned_prefixes = (
"capability_inventory",
"evidence_matrix")
715 for path
in GENERATED_DIR.iterdir()
716 if path.is_file()
and path.name.startswith(owned_prefixes)
718 orphans = sorted(existing - set(managed))
722 f
"stale: {path.relative_to(REPO_ROOT)}"
723 for path, content
in managed.items()
724 if not path.is_file()
or path.read_text(encoding=
"utf-8") != content
726 problems += [f
"orphan: {path.relative_to(REPO_ROOT)} (no family produces it)" for path
in orphans]
728 print(
"Generated capability inventory is out of date:", file=sys.stderr)
729 for problem
in problems:
730 print(f
" {problem}", file=sys.stderr)
731 print(
"\nRegenerate with: make docs-inventory", file=sys.stderr)
733 print(f
"Capability inventory is current ({len(inventory)} families, {len(managed)} managed files).")
736 for path, content
in managed.items():
737 path.write_text(content, encoding=
"utf-8")
740 print(f
"Removed orphaned generated file: {path.relative_to(REPO_ROOT)}")
743 f
"Wrote capability inventory: {len(inventory)} families, "
744 f
"{counts['selectable']} canonical, {counts['spelling']} accepted spelling, "
745 f
"{counts['alias']} deprecated alias, {counts['latent']} latent; "
746 f
"{len(managed)} managed files."
751if __name__ ==
"__main__":
752 raise SystemExit(
main())
int main()
Generate the capability inventory artifacts.
dict[str, int] classify(list[dict] inventory)
Count selectable, alias, and latent values separately.
dict python_constant_values(str module, str symbol)
Read the accepted values from a named module-level choice set.
set[str] c_dispatch_values(str path, str function, str prefix)
Extract the enum constants an if/else dispatch chain compares against.
Path family_fragment_path(str family_id)
Path of the per-family includable fragment.
set[str] c_enum_values(str path, str symbol)
Extract the members of a C enum.
dict load_registry()
Load the capability family registry.
str function_body(str path, str function)
Return the source text of one C function, so extraction never spans the file.
str entry_anchor(dict registry_entry, str value)
Anchor name of the Tier-2 entry for one selector value.
None apply_reachability(list[dict] inventory, dict registry)
Mark declared values that no other family can actually satisfy as latent.
dict python_equality_chain_values(str module, str symbol)
Read accepted values from a normalizer that compares against string literals.
dict collect(dict family)
Build one family's inventory record from its declared sources.
dict python_membership_values(str module, str symbol)
Read the accepted values from a normalizer that validates by set membership.
dict[str, dict] python_normalizer_values(str module, str symbol)
Read the canonical selector strings accepted by a normalizer function.
str html_escape(str text)
Escape text for inclusion in generated HTML.
str render_evidence_matrix(list[dict] inventory, dict registry)
Render the project-wide capability-by-evidence matrix as an HTML fragment.
set documented_entries(dict family, dict registry_entry)
Values whose Tier-2 entry anchor is actually present on the family page.
str render_family(dict family, dict registry_entry)
Render one family's value table as a Doxygen-includable HTML fragment.
literal(ast.AST node)
Evaluate a literal AST node, additionally accepting the bare set() call that appears in the boundary-...
dict[str, str] c_string_map_values(str path, str function)
Extract the case-insensitive selector strings a C parser accepts.
set[str] c_switch_values(str path, str function, str prefix)
Extract the enum constants a C factory switch dispatches on.
dict[str, dict] python_dict_values(str module, str symbol)
Read a public-surface dictionary from the CLI package without importing PETSc.
ast.Module module_syntax(str module)
Parse a dotted module into one syntax tree, whether file or package.
dict[str, str] c_token_map_values(str path, str function, str variable, str field)
Extract an exact-match token chain that assigns an enum to a context field.
str render_markdown(list[dict] inventory)
Render the inventory as a Doxygen-includable Markdown fragment.
None apply_metadata(list[dict] inventory, dict registry)
Merge declared per-value metadata (status, alias target) into the inventory.
Head of a generic C-style linked list.