@@ -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).
641656PAGES = [
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+
764792def 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