Skip to content

Commit c80c932

Browse files
HanSur94claude
andauthored
fix(docs): auto-discover API classes, fix FastPlot rename drift (#169)
generate_api_docs.py no longer relies on hand-maintained class lists, which had bitrotted with phantom classes (Sensor, ThresholdRule, EventDetector, FastSenseFigure) after the Tag/FastPlot refactors. Completeness now comes from directory discovery; the per-page lists are ordering-only anchors that cannot drop or duplicate a real class. - Rename the main API page FastSense -> FastPlot to match _Sidebar.md and the project brand; the run auto-prunes the now-orphaned FastSense page - Add prune_stale_api_pages(): safely removes API-Reference pages bearing this generator's banner that it no longer produces (manual pages and generate_wiki.py-owned pages are untouched) - Warn when a scanned library has classdefs but no page mapping - CLAUDE.md: correct stale anthropic/ANTHROPIC_API_KEY references to openai/OPENROUTER_API_KEY (generate_wiki.py calls the OpenRouter API) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 99d138f commit c80c932

7 files changed

Lines changed: 2368 additions & 3233 deletions

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,13 @@ An upgrade to FastSense's existing dashboard engine adding nested layout organiz
6161
- `uvicorn[standard] >= 0.24`
6262
- `websockets >= 12.0`
6363
- `numpy >= 1.24`
64-
- `anthropic` (dev/scripts dependency, NOT in main dependencies — used only by `scripts/generate_wiki.py`)
64+
- `openai` (dev/scripts dependency, NOT in main dependencies — used by `scripts/generate_wiki.py` to call the OpenRouter API)
6565
- GitHub Actions - CI/CD (tests, MEX build, benchmarks, wiki generation, release)
6666
- Codecov - test coverage reporting (MATLAB runs only; token via secret)
6767
## Configuration
6868
- `FASTSENSE_SKIP_BUILD=1` - skip MEX compilation in CI when MEX binaries are cached
6969
- `FASTSENSE_RESULTS_FILE` - path for Octave test result output in CI
70-
- `ANTHROPIC_API_KEY` - required only for `scripts/generate_wiki.py` (wiki auto-generation)
70+
- `OPENROUTER_API_KEY` - required only for `scripts/generate_wiki.py` (wiki auto-generation via OpenRouter)
7171
- `miss_hit.cfg` - MISS_HIT linter/style/metric configuration (project root)
7272
- `bridge/python/pyproject.toml` - Python bridge package config
7373
- ARM64: `-O3 -ffast-math` (Clang/MATLAB) or `-O3 -mcpu=apple-m3 -ftree-vectorize -ffast-math` (GCC/Octave)

scripts/generate_api_docs.py

Lines changed: 94 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -637,75 +637,58 @@ def _escape_md_table(text: str) -> str:
637637
# Page definitions
638638
# ---------------------------------------------------------------------------
639639

640-
# Each page: (output filename, page title, library dir, class order)
640+
# Each page: (output filename, page title, library dir, anchors).
641+
#
642+
# `anchors` is an ORDERING HINT, not an allow-list: for a library page, EVERY
643+
# classdef discovered in the directory is documented regardless of whether it
644+
# is listed here (see generate_page). Anchors float the headline classes to the
645+
# top; remaining classes are appended alphabetically. An anchor that no longer
646+
# exists is silently skipped, so this list can never drop or duplicate a real
647+
# class — it only affects ordering. Keep it short (the few most important
648+
# classes). This is what makes the generator immune to class renames.
649+
#
650+
# Exception: the special page with library dir None (Utilities) uses `anchors`
651+
# as an explicit cross-library class selector — only the named classes appear.
652+
#
653+
# Page names track the published wiki and _Sidebar.md (project brand:
654+
# "FastPlot"), which intentionally differs from the underlying class names
655+
# (e.g. the FastSense class is documented on the "FastPlot" page).
641656
PAGES = [
642657
(
643-
"API-Reference:-FastSense.md",
644-
"API Reference: FastSense",
658+
"API-Reference:-FastPlot.md",
659+
"API Reference: FastPlot",
645660
"FastSense",
646-
[
647-
"FastSense",
648-
"FastSenseFigure",
649-
"FastSenseDock",
650-
"FastSenseToolbar",
651-
"FastSenseTheme",
652-
"FastSenseDataStore",
653-
"NavigatorOverlay",
654-
"SensorDetailPlot",
655-
],
661+
["FastSense", "FastSenseDataStore", "FastSenseGrid", "SensorDetailPlot"],
656662
),
657663
(
658664
"API-Reference:-Dashboard.md",
659665
"API Reference: Dashboard",
660666
"Dashboard",
661667
[
662-
"DashboardEngine",
663-
"DashboardBuilder",
664-
"DashboardWidget",
665-
"FastSenseWidget",
666-
"GaugeWidget",
667-
"NumberWidget",
668-
"StatusWidget",
669-
"TextWidget",
670-
"TableWidget",
671-
"RawAxesWidget",
672-
"EventTimelineWidget",
673-
"DashboardSerializer",
674-
"DashboardLayout",
675-
"DashboardTheme",
676-
"DashboardToolbar",
668+
"DashboardEngine", "DashboardBuilder", "DashboardWidget",
669+
"DashboardLayout", "DashboardPage", "DashboardSerializer",
677670
],
678671
),
679672
(
680673
"API-Reference:-Sensors.md",
681674
"API Reference: Sensors",
682675
"SensorThreshold",
683-
["Sensor", "StateChannel", "ThresholdRule", "SensorRegistry"],
676+
[
677+
"Tag", "SensorTag", "MonitorTag", "CompositeTag", "DerivedTag",
678+
"StateTag", "TagRegistry",
679+
],
684680
),
685681
(
686682
"API-Reference:-Event-Detection.md",
687683
"API Reference: Event Detection",
688684
"EventDetection",
689-
[
690-
"EventDetector",
691-
"IncrementalEventDetector",
692-
"Event",
693-
"EventConfig",
694-
"EventStore",
695-
"EventViewer",
696-
"LiveEventPipeline",
697-
"NotificationService",
698-
"NotificationRule",
699-
"DataSource",
700-
"MatFileDataSource",
701-
"DataSourceMap",
702-
],
685+
["Event", "EventStore", "EventViewer", "LiveEventPipeline"],
703686
),
704687
(
705688
"API-Reference:-Utilities.md",
706689
"API Reference: Utilities",
707-
None, # special: pulls from multiple dirs
708-
["ConsoleProgressBar", "FastSenseDefaults"],
690+
None, # special: explicit cross-library class list (anchors IS the selector)
691+
["ConsoleProgressBar"],
709692
),
710693
]
711694

@@ -727,18 +710,24 @@ def collect_classes(lib_dir: Path) -> dict:
727710
return classes
728711

729712

730-
def generate_page(filename, title, classes_by_name, class_order):
731-
"""Generate a wiki page for the given classes."""
713+
def generate_page(filename, title, classes_by_name, anchors):
714+
"""Generate a wiki page documenting every class in classes_by_name.
715+
716+
`anchors` lists headline class names to emit first, in order; every
717+
remaining class is appended alphabetically. Anchor names absent from
718+
classes_by_name are skipped — completeness comes from discovery, never
719+
from the anchor list, so a stale anchor cannot drop a real class.
720+
"""
732721
parts = [AUTO_GENERATED_NOTICE, f"# {title}\n"]
733722

734723
written = set()
735-
for name in class_order:
724+
for name in anchors:
736725
if name in classes_by_name:
737726
parts.append(format_class_markdown(classes_by_name[name]))
738727
parts.append("---\n")
739728
written.add(name)
740729

741-
# Append any classes found but not in the explicit order
730+
# Append any discovered classes not already emitted via anchors.
742731
for name, cls in sorted(classes_by_name.items()):
743732
if name not in written:
744733
parts.append(format_class_markdown(cls))
@@ -761,8 +750,47 @@ def generate_page(filename, title, classes_by_name, class_order):
761750
# Main
762751
# ---------------------------------------------------------------------------
763752

753+
# Library directories scanned for classdefs. Order is cosmetic (parse logging).
754+
SCANNED_LIBS = ["FastSense", "Dashboard", "SensorThreshold", "EventDetection", "WebBridge"]
755+
756+
# Scanned for cross-page class lookup but intentionally given no page of their
757+
# own. Suppresses the "undocumented library" warning below.
758+
UNDOCUMENTED_OK = {"WebBridge"}
759+
760+
761+
def prune_stale_api_pages(produced):
762+
"""Delete API-Reference pages this generator owns but no longer produces.
763+
764+
Safety: only files matching ``API-Reference:-*.md`` that carry THIS
765+
script's auto-generated banner are eligible. Manually-maintained pages
766+
(e.g. API-Reference:-Themes.md, which documents a function rather than a
767+
classdef) carry no banner and are never touched; nor are pages owned by
768+
generate_wiki.py, which use a different banner. This makes orphans left by
769+
a page rename self-healing instead of silently shipping stale content.
770+
"""
771+
banner_marker = "by scripts/generate_api_docs.py"
772+
for page in sorted(WIKI_DIR.glob("API-Reference:-*.md")):
773+
if page.name in produced:
774+
continue
775+
head = page.read_text(encoding="utf-8", errors="replace")[:300]
776+
if banner_marker in head:
777+
page.unlink()
778+
print(f" -> Pruned stale auto-generated page: {page.relative_to(PROJECT_ROOT)}")
779+
780+
781+
def warn_undocumented_libs(lib_classes, documented_libs):
782+
"""Warn if a scanned library has classdefs but no page maps to it."""
783+
for lib_name, parsed in lib_classes.items():
784+
if parsed and lib_name not in documented_libs and lib_name not in UNDOCUMENTED_OK:
785+
print(
786+
f" WARNING: libs/{lib_name}/ has {len(parsed)} class(es) but no "
787+
f"API-Reference page maps to it — add an entry to PAGES.",
788+
file=sys.stderr,
789+
)
790+
791+
764792
def main():
765-
print(f"FastSense API Doc Generator")
793+
print("FastPlot API Doc Generator")
766794
print(f"Project root: {PROJECT_ROOT}")
767795
print(f"Libs dir: {LIBS_DIR}")
768796
print(f"Wiki dir: {WIKI_DIR}")
@@ -778,7 +806,7 @@ def main():
778806
all_classes = {} # name -> MatlabClass
779807
lib_classes = {} # lib_name -> {name: MatlabClass}
780808

781-
for lib_name in ["FastSense", "Dashboard", "SensorThreshold", "EventDetection", "WebBridge"]:
809+
for lib_name in SCANNED_LIBS:
782810
lib_dir = LIBS_DIR / lib_name
783811
print(f"[{lib_name}]")
784812
parsed = collect_classes(lib_dir)
@@ -788,18 +816,29 @@ def main():
788816

789817
# Generate pages
790818
print("Generating wiki pages:")
791-
for filename, title, lib_name, class_order in PAGES:
819+
produced = set()
820+
documented_libs = set()
821+
for filename, title, lib_name, anchors in PAGES:
792822
if lib_name is None:
793-
# Utilities page: pull from all_classes
823+
# Special page: anchors doubles as an explicit cross-library class
824+
# selector — only the named classes are documented here.
794825
subset = {
795826
name: all_classes[name]
796-
for name in class_order
827+
for name in anchors
797828
if name in all_classes
798829
}
799830
else:
831+
# Library page: document every classdef discovered in the directory.
800832
subset = lib_classes.get(lib_name, {})
833+
documented_libs.add(lib_name)
834+
835+
generate_page(filename, title, subset, anchors)
836+
produced.add(filename)
801837

802-
generate_page(filename, title, subset, class_order)
838+
# Remove pages we used to generate but no longer do (e.g. after a rename),
839+
# then flag any library that has classes but no page mapping (inverse drift).
840+
prune_stale_api_pages(produced)
841+
warn_undocumented_libs(lib_classes, documented_libs)
803842

804843
print()
805844
print("Done. Generated API reference pages in wiki/.")

0 commit comments

Comments
 (0)