Skip to content

Python API Reference

This page contains the programmatic Python API reference for building specifications. Documentation for these interfaces is extracted dynamically from the source code.


Specification Primitives

The core primitives are used to define context boundaries, requirements, and features within spec modules.

libspec.spec.Ctx

Source code in libspec/spec.py
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
class Ctx:
    # Return all classes in the MRO excluding Ctx and object.
    def _non_root_mro_classes(self):
        return [cls for cls in self.__class__.__mro__[1:] if cls not in (Ctx, object)]

    # Return all Ctx-derived classes in the MRO.
    def _inherited_ctx_classes(self):
        classes = []
        for cls in self._non_root_mro_classes():
            if issubclass(cls, Ctx):
                classes.append(cls)
        return classes

    # Collect values from zero-argument methods in inherited Ctx classes.
    def _inherited_field_values(self):
        values = {}
        for cls in self._inherited_ctx_classes():
            values.update(self._class_zero_arg_values(cls))
        return values

    # Return field values defined in a specific class's methods.
    def _class_zero_arg_values(self, cls):
        values = {}
        for name in dir(cls):
            if self._skip_ctx_member(name):
                continue
            value = self._evaluate_zero_arg_class_member(cls, name)
            if value is not _Missing:
                values[f"{cls.__name__}.{name}"] = value
        return values

    # Safely evaluate a zero-argument method from a class.
    def _evaluate_zero_arg_class_member(self, cls, name):
        try:
            member = getattr(cls, name, None)
            if not callable(member):
                return member
            if isinstance(member, type):
                return member
            sig = signature(member)
            params = list(sig.parameters.values())
            if len(params) == 0:
                return member()
            elif len(params) == 1 and params[0].name == "self":
                return member(self)
            return _Missing
        except Exception:
            return _Missing

    # Return True if the member name should be skipped during context collection.
    def _skip_ctx_member(self, name):
        return name.startswith("_") or name in CTX_RESERVED_NAMES

    # Identify fields that override values defined in parent classes.
    def _detect_overrides(self):
        overrides = []
        parent_values = self._inherited_field_values()
        current_ctx = self.ctx(template_only=False)

        for key, value in current_ctx.items():
            if self._is_overridden_value(key, value, parent_values):
                overrides.append(key)
        return overrides

    # Return True if the field value differs from any parent class definition.
    def _is_overridden_value(self, key, value, parent_values):
        for parent_key, parent_value in parent_values.items():
            if key == parent_key.split(".")[-1] and value != parent_value:
                return True
        return False

    # Calculate the set of requirements that differ from parent classes.
    def _delta_requirements(self):
        deltas = {}
        own_doc = self._instance_notes()
        parent_docs = set(self._inherited_docstrings())
        if own_doc and own_doc not in parent_docs:
            deltas["notes"] = own_doc

        for key, value in self.ctx(template_only=False).items():
            if key == "_in_ctx":
                continue
            if not self._parent_has_same_value(key, value):
                deltas[key] = value
        return deltas

    # Return a list of docstrings from all inherited classes.
    def _inherited_docstrings(self):
        docs = []
        for cls in self._non_root_mro_classes():
            if cls.__doc__:
                docs.append(cleandoc(cls.__doc__))
        return docs

    # Return True if any parent class shares the same field value.
    def _parent_has_same_value(self, key, value):
        for cls in self._non_root_mro_classes():
            if not hasattr(cls, key):
                continue
            parent_val = self._safe_call(getattr(cls, key))
            if parent_val is _Missing:
                continue
            if parent_val == value:
                return True
        return False

    # Safely invoke a callable and return its result or _Missing on failure.
    def _safe_call(self, maybe_callable):
        if not callable(maybe_callable):
            return maybe_callable
        if isinstance(maybe_callable, type):
            return maybe_callable
        try:
            sig = signature(maybe_callable)
            params = list(sig.parameters.values())
            if len(params) == 0:
                return maybe_callable()
            elif len(params) == 1 and params[0].name == "self":
                return maybe_callable(self)
            return _Missing
        except Exception:
            return _Missing

    # Collect FQNs of all Requirement-derived classes in the MRO.
    def _effective_requirement_ids(self):
        from .spec_types import Requirement

        req_ids = []
        for cls in self.__class__.__mro__:
            if not issubclass(cls, Requirement) or cls is Requirement:
                continue
            try:
                class_id = fqn(cls)
                if class_id:
                    req_ids.append(class_id)
            except Exception:
                continue
        return req_ids

    # Concatenate docstring templates from parent classes.
    def _base_template(self):
        templates = []
        for cls in self._non_root_mro_classes():
            cleaned = self._class_docstring(cls)
            if cleaned:
                templates.append(cleaned)
        templates.reverse()
        return "\n\n".join(templates)

    # Return the cleaned docstring for a given class.
    def _class_docstring(self, cls):
        doc = cls.__doc__
        return cleandoc(doc) if doc else ""

    # Return the docstring template for the current class.
    def _compiled_docstring_template(self):
        return self._class_docstring(self.__class__)

    # Return notes specific to the current instance (its docstring).
    def _instance_notes(self):
        return self._compiled_docstring_template()

    # Return source location metadata for an object or the current class.
    def _source_info(self, obj=None):
        target = obj if obj is not None else self.__class__
        try:
            source_file = inspect.getsourcefile(target)
            source_file = os.path.abspath(source_file) if source_file else None
            lines, start_line = inspect.getsourcelines(target)
            return {
                "file": source_file,
                "start_line": start_line,
                "end_line": start_line + len(lines) - 1,
                "name": getattr(target, "__name__", str(target)),
            }
        except (OSError, TypeError):
            return None

    # Build and return the context data used for template rendering.
    def ctx(self, template_only=True):
        if getattr(self, "_in_ctx", False):
            return {}
        self._in_ctx = True
        try:
            return self._build_context(template_only)
        finally:
            self._in_ctx = False

    # Internal method to assemble the template context.
    def _build_context(self, template_only=True):
        expected = self._expected_template_vars()
        context = self._collect_template_context(expected)
        if template_only:
            return context
        return self._collect_non_template_context(context)

    # Identify all undeclared variables in the docstring templates.
    def _expected_template_vars(self):
        env = Environment()
        template_text = f"{self._base_template()}\n{self._instance_notes()}"
        return meta.find_undeclared_variables(env.parse(template_text))

    # Resolve and collect values for all required template variables.
    def _collect_template_context(self, expected_vars):
        context = {}
        if "fields" in expected_vars and hasattr(self, "fields"):
            context["fields"] = self.fields()

        for var_name in sorted(expected_vars):
            if var_name == "fields":
                continue
            context[var_name] = self._resolve_template_var(var_name)
        return context

    # Map template variable names to methods or attributes and return their values.
    def _resolve_template_var(self, var_name):
        member_name = var_name.replace("-", "_")
        if hasattr(self, member_name):
            member = getattr(self, member_name)
            if callable(member) and not isinstance(member, type):
                try:
                    sig = signature(member)
                    params = list(sig.parameters.values())
                    if len(params) == 0:
                        return member()
                    elif len(params) == 1 and params[0].name == "self":
                        return member()
                except Exception:
                    pass
            return member

        # Check annotations in class MRO for default value or missing value declaration
        cls = self.__class__
        for base in cls.__mro__:
            if hasattr(base, "__annotations__") and member_name in base.__annotations__:
                if hasattr(self, member_name):
                    return getattr(self, member_name)
                raise AttributeError(
                    f"Field '{member_name}' is declared via type annotations but lacks a value."
                )

        raise AttributeError(self._missing_template_var_message(var_name, member_name))

    # Generate a descriptive error message for missing template variables.
    def _missing_template_var_message(self, var_name, member_name):
        src = self._source_info()
        location = f"{src['file']}:{src['start_line']}" if src else "unknown location"
        return (
            f"\nThe variable '{{{{{var_name}}}}}' was found in a docstring template "
            f"for class '{self.__class__.__name__}',\n"
            f"defined at {location},\n"
            f"but no matching method or attribute '{member_name}' was found.\n\n"
            f"FIX: implement 'def {member_name}(self):' in class "
            f"'{self.__class__.__name__}' or one of its bases.\n"
        )

    # Supplement the context with all available public methods and attributes.
    def _collect_non_template_context(self, context):
        for name in sorted(dir(self)):
            if self._skip_runtime_context_member(name, context):
                continue
            value = self._resolve_runtime_context_member(name)
            if value is not _Missing:
                context[name] = value
        return context

    # Return True if a member should be excluded from the runtime context.
    def _skip_runtime_context_member(self, name, context):
        if name.startswith("_") or name in CTX_RESERVED_NAMES:
            return True
        if name in CTX_INTERNAL_NAMES or name in context:
            return True
        return False

    # Resolve a member name to its value for runtime context inclusion.
    def _resolve_runtime_context_member(self, name):
        member = getattr(self, name)
        if not callable(member):
            return member
        try:
            if len(signature(member).parameters) != 0:
                return _Missing
            return member()
        except (TypeError, ValueError, UnimplementedMethodError):
            return _Missing

    # Recursively convert Python values to XML elements.
    def _to_xml_element(self, name, value):
        elem = ET.Element(name)
        if isinstance(value, dict):
            for key in sorted(value.keys()):
                if key in SKIPPED_SOURCE_LINE_KEYS:
                    continue
                child_name = str(key).replace("-", "_")
                elem.append(self._to_xml_element(child_name, value[key]))
            return elem

        if isinstance(value, list):
            for item in value:
                elem.append(self._to_xml_element("item", item))
            return elem

        elem.text = str(value)
        return elem

    # Render the specification as structured XML using minidom for formatting.
    def render_xml(self):
        try:
            return minidom.parseString(
                ET.tostring(self.to_xml_element(), encoding="utf-8")
            ).toprettyxml(indent="  ")
        except Exception as e:
            return f"<!-- Error rendering XML: {e} -->\n" + ET.tostring(
                self.to_xml_element(), encoding="unicode"
            )

    # Build the XML representation of the specification.
    def to_xml_element(self):
        """Convert the specification to an XML element."""
        root = ET.Element("specification")
        root.set("type", self.__class__.__name__)
        root.set("ref", fqn(self.__class__))
        ctx_data = self.ctx()
        self._append_source_metadata(root)
        self._append_docstring(root, ctx_data)
        self._append_context(root)
        self._append_inheritance(root)
        self._append_effective_req_ids(root)
        self._append_overrides(root)
        self._append_delta_requirements(root)
        return root

    # Append source file and line information to the XML element.
    def _append_source_metadata(self, root):
        src = self._source_info()
        if not src:
            return
        source_elem = ET.SubElement(root, "source")
        source_elem.set("target", src["name"])
        source_elem.set("file", src["file"])

    # Render and append the docstring to the XML element.
    def _append_docstring(self, root, ctx_data):
        template_text = self._compiled_docstring_template()
        if not template_text:
            return
        try:
            rendered = Template(template_text).render(**ctx_data).strip()
            docstring_elem = ET.SubElement(root, "docstring")
            docstring_elem.text = rendered
        except Exception as e:
            print(f"Error rendering docstring for {self.__class__.__name__}: {e}")

    # Append all context fields to the XML element.
    def _append_context(self, root):
        context_elem = ET.SubElement(root, "context")
        for key, value in sorted(self.ctx(template_only=False).items()):
            name = str(key).replace("-", "_")
            context_elem.append(self._to_xml_element(name, value))

    # Append inheritance references to the XML element.
    def _append_inheritance(self, root):
        inherited = [
            cls for cls in self._non_root_mro_classes() if self._class_docstring(cls)
        ]
        if not inherited:
            return
        inherits_elem = ET.SubElement(root, "inherits")
        for cls in inherited:
            try:
                ref_elem = self._to_xml_element("ref", fqn(cls))
                inherits_elem.append(ref_elem)
            except Exception:
                continue

    # Append all effective requirement IDs to the XML element.
    def _append_effective_req_ids(self, root):
        req_ids = self._effective_requirement_ids()
        if not req_ids:
            return
        req_elem = ET.SubElement(root, "effective_req_ids")
        for req_id in req_ids:
            req_elem.append(self._to_xml_element("id", req_id))

    # Append field override information to the XML element.
    def _append_overrides(self, root):
        overrides = self._detect_overrides()
        if not overrides:
            return
        overrides_elem = ET.SubElement(root, "overrides")
        for name in overrides:
            overrides_elem.append(self._to_xml_element("field", name))

    # Append requirement deltas to the XML element.
    def _append_delta_requirements(self, root):
        deltas = self._delta_requirements()
        if not deltas:
            return
        delta_elem = ET.SubElement(root, "delta_requirements")
        for key, value in deltas.items():
            name = str(key).replace("-", "_")
            delta_elem.append(self._to_xml_element(name, value))

Methods:

to_xml_element()

Convert the specification to an XML element.

Source code in libspec/spec.py
def to_xml_element(self):
    """Convert the specification to an XML element."""
    root = ET.Element("specification")
    root.set("type", self.__class__.__name__)
    root.set("ref", fqn(self.__class__))
    ctx_data = self.ctx()
    self._append_source_metadata(root)
    self._append_docstring(root, ctx_data)
    self._append_context(root)
    self._append_inheritance(root)
    self._append_effective_req_ids(root)
    self._append_overrides(root)
    self._append_delta_requirements(root)
    return root

libspec.spec_types.Requirement

Bases: Ctx

Requirement TITLE: {{title}} REQUIREMENT-ID: {{req_id}}

Insert REQUIREMENT-ID into any source code for cross reference purposes.

Source code in libspec/spec_types.py
class Requirement(Ctx):
    """
    Requirement
    TITLE: {{title}}
    REQUIREMENT-ID: {{req_id}}

    Insert REQUIREMENT-ID into any source code for cross reference purposes.
    """

    # Return the title of the requirement.
    def title(self):
        return self.__class__.__name__

    # Return the ID of the requirement.
    def req_id(self):
        return fqn(self)

libspec.spec_types.Feature

Bases: Ctx

Feature Specification: {{feature_name}}

Source code in libspec/spec_types.py
class Feature(Ctx):
    """
    Feature Specification: {{feature_name}}

    """

    # Return the name of the feature based on its class name.
    def feature_name(self):
        return self.__class__.__name__

    # Return the date associated with the feature.
    def date(self):
        raise UnimplementedMethodError()

    # Return the description of the feature.
    def description(self):
        raise UnimplementedMethodError()

libspec.spec.Spec

Source code in libspec/spec.py
class Spec:
    # Return the list of modules that contain specifications.
    def modules(self):
        raise UnimplementedMethodError()

    # Generate the complete specification as a structured XML document.
    def generate_xml(self):
        root = self._build_specification_set()
        return self._pretty_xml(root)

    # Pretty-print the XML root element.
    def _pretty_xml(self, element):
        try:
            xml_bytes = ET.tostring(element, encoding="utf-8")
            return minidom.parseString(xml_bytes).toprettyxml(indent="  ")
        except Exception as e:
            return f"<!-- Error formatting XML: {e} -->\n" + ET.tostring(
                element, encoding="unicode"
            )

    # Build the complete specification set as an XML element.
    def _build_specification_set(self):
        root = ET.Element("specification_set")
        root.set("libspec-version", get_libspec_version())
        self._append_module_spec_elements(root)
        return root

    # Append specification elements from all modules to the root.
    def _append_module_spec_elements(self, root):
        emitted_refs = set()
        all_module_specs = []
        for mod in self.modules():
            all_module_specs.extend(instantiate_module_specs(mod))

        # Pass 1: emit all full module-defined specs first so no class is
        # eclipsed by a thin dependency stub added when processing a sibling
        # that happens to sort earlier (e.g. HumanTask before Task).
        for spec in all_module_specs:
            self._append_spec(root, spec.to_xml_element(), emitted_refs)

        # Pass 2: emit dependency stubs for inherited classes not already seen
        for spec in all_module_specs:
            self._append_inherited_dependencies(root, spec, emitted_refs)

    # Append a single specification element to the root, avoiding duplicates.
    def _append_spec(self, root, element, emitted_refs):
        ref = element.get("ref") or element.get("type")
        if not ref or ref in emitted_refs:
            return
        root.append(element)
        emitted_refs.add(ref)

    # Recursively append all inherited dependency specifications to the root.
    def _append_inherited_dependencies(self, root, spec, emitted_refs):
        pending = [
            cls
            for cls in spec._non_root_mro_classes()
            if self._docstring_template_for_class(cls)
        ]
        while pending:
            cls = pending.pop(0)
            dep_ref = fqn(cls)
            if dep_ref in emitted_refs:
                continue

            dep_elem = self._dependency_spec_element(cls)
            self._append_spec(root, dep_elem, emitted_refs)

            for parent in cls.__mro__[1:]:
                if parent in (Ctx, object):
                    continue
                if self._docstring_template_for_class(parent):
                    pending.append(parent)

    # Create an XML element representing a dependency specification for a class.
    def _dependency_spec_element(self, cls):
        elem = ET.Element("specification")
        elem.set("type", cls.__name__)
        elem.set("ref", fqn(cls))
        elem.set("dependency", "true")

        template_text = self._docstring_template_for_class(cls)
        is_template = "{{" in template_text or "{%" in template_text
        elem.set("template", "true" if is_template else "false")

        source_info = self._source_info_for_class(cls)
        if source_info:
            source_elem = ET.SubElement(elem, "source")
            source_elem.set("target", source_info["name"])
            source_elem.set("file", source_info["file"])

        template_text = self._docstring_template_for_class(cls)
        if template_text:
            docstring_template_elem = ET.SubElement(elem, "docstring_template")
            docstring_template_elem.text = template_text

        inherited = [
            parent
            for parent in cls.__mro__[1:]
            if parent not in (Ctx, object)
            and self._docstring_template_for_class(parent)
        ]
        if inherited:
            inherits_elem = ET.SubElement(elem, "inherits")
            for parent in inherited:
                parent_ref = ET.SubElement(inherits_elem, "ref")
                parent_ref.text = fqn(parent)
        return elem

    # Return source file and line information for a given class.
    def _source_info_for_class(self, cls):
        try:
            source_file = inspect.getsourcefile(cls)
            source_file = os.path.abspath(source_file) if source_file else None
            lines, start_line = inspect.getsourcelines(cls)
            return {
                "file": source_file,
                "start_line": start_line,
                "end_line": start_line + len(lines) - 1,
                "name": getattr(cls, "__name__", str(cls)),
            }
        except (OSError, TypeError):
            return None

    # Return the cleaned docstring template for a given class.
    def _docstring_template_for_class(self, cls):
        return _clean_doc(cls)

    def get_components(self):
        """Compile specifications from all modules into Component dataclasses."""
        import hashlib

        from libspec.store import Component

        emitted_refs = set()
        components = []
        all_module_specs = []
        for mod in self.modules():
            all_module_specs.extend(instantiate_module_specs(mod))

        # Collect full specs first
        for spec in all_module_specs:
            ref = fqn(spec.__class__)
            if ref in emitted_refs:
                continue

            template_text = self._docstring_template_for_class(spec.__class__)
            is_template = "{{" in template_text or "{%" in template_text

            if is_template:
                ctx_data = spec.ctx()
                try:
                    docstring = Template(template_text).render(**ctx_data).strip()
                except Exception as e:
                    print(
                        f"Error rendering template docstring for {spec.__class__.__name__}: {e}"
                    )
                    docstring = template_text
            else:
                docstring = template_text

            inherited = [
                fqn(parent)
                for parent in spec.__class__.__mro__[1:]
                if parent not in (Ctx, object)
                and self._docstring_template_for_class(parent)
            ]

            comp_hash = hashlib.sha256(docstring.encode("utf-8")).hexdigest()

            components.append(
                Component(
                    ref=ref,
                    docstring=docstring,
                    is_template=is_template,
                    inherits=inherited,
                    hash=comp_hash,
                    is_dependency=False,
                )
            )
            emitted_refs.add(ref)

        # Collect inherited dependencies not already emitted
        for spec in all_module_specs:
            pending = [
                cls
                for cls in spec._non_root_mro_classes()
                if self._docstring_template_for_class(cls)
            ]
            while pending:
                cls = pending.pop(0)
                dep_ref = fqn(cls)
                if dep_ref in emitted_refs:
                    continue

                template_text = self._docstring_template_for_class(cls)
                is_template = "{{" in template_text or "{%" in template_text

                if is_template:
                    try:
                        dep_instance = cls()
                        ctx_data = dep_instance.ctx()
                        docstring = Template(template_text).render(**ctx_data).strip()
                    except Exception as e:
                        print(
                            f"Error rendering template docstring for {cls.__name__}: {e}"
                        )
                        docstring = template_text
                else:
                    docstring = template_text

                inherited = [
                    fqn(parent)
                    for parent in cls.__mro__[1:]
                    if parent not in (Ctx, object)
                    and self._docstring_template_for_class(parent)
                ]

                comp_hash = hashlib.sha256(docstring.encode("utf-8")).hexdigest()

                components.append(
                    Component(
                        ref=dep_ref,
                        docstring=docstring,
                        is_template=is_template,
                        inherits=inherited,
                        hash=comp_hash,
                        is_dependency=True,
                    )
                )
                emitted_refs.add(dep_ref)

                for parent in cls.__mro__[1:]:
                    if parent in (Ctx, object):
                        continue
                    if self._docstring_template_for_class(parent):
                        pending.append(parent)

        return components

    # Write the XML specification and source map to the output directory.
    def write_xml(self, output_dir=None):
        """Write the XML specification to a hashed file in the given directory."""
        components = self.get_components()

        # Compute deterministic master hash and snapshot ID
        import hashlib

        sorted_components = sorted(components, key=lambda c: c.ref)
        hasher = hashlib.sha256()
        for comp in sorted_components:
            hasher.update(comp.ref.encode("utf-8"))
            hasher.update(comp.hash.encode("utf-8"))
        master_hash = hasher.hexdigest()
        snapshot_id = master_hash[:16]

        # If output_dir is provided, write the serialized XML specification directly
        if output_dir:
            xml_content = self.generate_xml()
            path = self._spec_output_path(output_dir, xml_content)
            # Inject date-created timestamp into the written file content
            written_content = self._inject_date_created(xml_content)
            with open(path, "w", encoding="utf-8") as f:
                f.write(written_content)
            print(f"Specification written to {path}")
            return path
        else:
            print(
                f"Specification compiled successfully. (ID: {snapshot_id}, Hash: {master_hash})"
            )
            return None

    # Calculate the hashed output path for the XML specification.
    def _spec_output_path(self, output_dir, xml_content):
        os.makedirs(output_dir, exist_ok=True)
        digest = easy_hash(xml_content)[:20]
        return os.path.join(output_dir, f"spec-{digest}.xml")

    # Inject the current timestamp into the specification set element.
    def _inject_date_created(self, xml_content):
        created_at = datetime.datetime.now().astimezone().isoformat()
        return xml_content.replace(
            "<specification_set",
            f'<specification_set date-created="{created_at}"',
            1,
        )

    # Write text content to a file, handling potential IO errors.
    def _write_text(self, path, content):
        try:
            with open(path, "w", encoding="utf-8") as f:
                f.write(content)
        except OSError as e:
            print(f"Error writing to {path}: {e}")

    # Process command line arguments for standalone specification generation.
    def handle_cli(self):
        """Handle command line interface for specification generation."""
        parser = argparse.ArgumentParser(description="libspec CLI")
        parser.add_argument(
            "-o", "--output", help="Output directory for XML specification"
        )
        parser.add_argument(
            "--xml", action="store_true", help="Print XML specification to stdout"
        )
        args = parser.parse_args()

        if args.output:
            self.write_xml(args.output)
            return
        if args.xml:
            print(self.generate_xml())
            return
        self.generate_xml()

Methods:

get_components()

Compile specifications from all modules into Component dataclasses.

Source code in libspec/spec.py
def get_components(self):
    """Compile specifications from all modules into Component dataclasses."""
    import hashlib

    from libspec.store import Component

    emitted_refs = set()
    components = []
    all_module_specs = []
    for mod in self.modules():
        all_module_specs.extend(instantiate_module_specs(mod))

    # Collect full specs first
    for spec in all_module_specs:
        ref = fqn(spec.__class__)
        if ref in emitted_refs:
            continue

        template_text = self._docstring_template_for_class(spec.__class__)
        is_template = "{{" in template_text or "{%" in template_text

        if is_template:
            ctx_data = spec.ctx()
            try:
                docstring = Template(template_text).render(**ctx_data).strip()
            except Exception as e:
                print(
                    f"Error rendering template docstring for {spec.__class__.__name__}: {e}"
                )
                docstring = template_text
        else:
            docstring = template_text

        inherited = [
            fqn(parent)
            for parent in spec.__class__.__mro__[1:]
            if parent not in (Ctx, object)
            and self._docstring_template_for_class(parent)
        ]

        comp_hash = hashlib.sha256(docstring.encode("utf-8")).hexdigest()

        components.append(
            Component(
                ref=ref,
                docstring=docstring,
                is_template=is_template,
                inherits=inherited,
                hash=comp_hash,
                is_dependency=False,
            )
        )
        emitted_refs.add(ref)

    # Collect inherited dependencies not already emitted
    for spec in all_module_specs:
        pending = [
            cls
            for cls in spec._non_root_mro_classes()
            if self._docstring_template_for_class(cls)
        ]
        while pending:
            cls = pending.pop(0)
            dep_ref = fqn(cls)
            if dep_ref in emitted_refs:
                continue

            template_text = self._docstring_template_for_class(cls)
            is_template = "{{" in template_text or "{%" in template_text

            if is_template:
                try:
                    dep_instance = cls()
                    ctx_data = dep_instance.ctx()
                    docstring = Template(template_text).render(**ctx_data).strip()
                except Exception as e:
                    print(
                        f"Error rendering template docstring for {cls.__name__}: {e}"
                    )
                    docstring = template_text
            else:
                docstring = template_text

            inherited = [
                fqn(parent)
                for parent in cls.__mro__[1:]
                if parent not in (Ctx, object)
                and self._docstring_template_for_class(parent)
            ]

            comp_hash = hashlib.sha256(docstring.encode("utf-8")).hexdigest()

            components.append(
                Component(
                    ref=dep_ref,
                    docstring=docstring,
                    is_template=is_template,
                    inherits=inherited,
                    hash=comp_hash,
                    is_dependency=True,
                )
            )
            emitted_refs.add(dep_ref)

            for parent in cls.__mro__[1:]:
                if parent in (Ctx, object):
                    continue
                if self._docstring_template_for_class(parent):
                    pending.append(parent)

    return components

handle_cli()

Handle command line interface for specification generation.

Source code in libspec/spec.py
def handle_cli(self):
    """Handle command line interface for specification generation."""
    parser = argparse.ArgumentParser(description="libspec CLI")
    parser.add_argument(
        "-o", "--output", help="Output directory for XML specification"
    )
    parser.add_argument(
        "--xml", action="store_true", help="Print XML specification to stdout"
    )
    args = parser.parse_args()

    if args.output:
        self.write_xml(args.output)
        return
    if args.xml:
        print(self.generate_xml())
        return
    self.generate_xml()

write_xml(output_dir=None)

Write the XML specification to a hashed file in the given directory.

Source code in libspec/spec.py
def write_xml(self, output_dir=None):
    """Write the XML specification to a hashed file in the given directory."""
    components = self.get_components()

    # Compute deterministic master hash and snapshot ID
    import hashlib

    sorted_components = sorted(components, key=lambda c: c.ref)
    hasher = hashlib.sha256()
    for comp in sorted_components:
        hasher.update(comp.ref.encode("utf-8"))
        hasher.update(comp.hash.encode("utf-8"))
    master_hash = hasher.hexdigest()
    snapshot_id = master_hash[:16]

    # If output_dir is provided, write the serialized XML specification directly
    if output_dir:
        xml_content = self.generate_xml()
        path = self._spec_output_path(output_dir, xml_content)
        # Inject date-created timestamp into the written file content
        written_content = self._inject_date_created(xml_content)
        with open(path, "w", encoding="utf-8") as f:
            f.write(written_content)
        print(f"Specification written to {path}")
        return path
    else:
        print(
            f"Specification compiled successfully. (ID: {snapshot_id}, Hash: {master_hash})"
        )
        return None

Specification Representation

These models represent compiled specifications within snapshots.

libspec.store.Component dataclass

Source code in libspec/common.py
@dataclass(frozen=True)
class Component:
    ref: str
    docstring: str
    is_template: bool
    inherits: list[str]
    hash: str
    is_dependency: bool = False

    def __post_init__(self):
        if not isinstance(self.ref, str) or not self.ref.strip():
            raise ValueError("Component 'ref' must be a non-empty string.")
        if not isinstance(self.docstring, str):
            raise TypeError("Component 'docstring' must be a string.")
        if not isinstance(self.is_template, bool):
            raise TypeError("Component 'is_template' must be a boolean.")
        if not isinstance(self.inherits, list) or not all(
            isinstance(x, str) for x in self.inherits
        ):
            raise TypeError("Component 'inherits' must be a list of strings.")
        if not isinstance(self.hash, str) or len(self.hash) != 64:
            raise ValueError(
                "Component 'hash' must be a 64-character SHA-256 hash string."
            )
        if not isinstance(self.is_dependency, bool):
            raise TypeError("Component 'is_dependency' must be a boolean.")

Specification Storage Models

The models representing snapshots and implementation mappings.

libspec.common.Snapshot dataclass

Source code in libspec/common.py
@dataclass(frozen=True)
class Snapshot:
    id: str
    created_at: datetime.datetime
    master_hash: str
    git_commit: str | None = None

    def __post_init__(self):
        if not isinstance(self.id, str) or not self.id.strip():
            raise ValueError("Snapshot 'id' must be a non-empty string.")
        if not isinstance(self.created_at, datetime.datetime):
            raise TypeError("Snapshot 'created_at' must be a datetime object.")
        if not isinstance(self.master_hash, str) or len(self.master_hash) not in (
            40,
            64,
        ):
            raise ValueError(
                "Snapshot 'master_hash' must be a 40-character or 64-character hex string."
            )
        if self.git_commit is not None and not isinstance(self.git_commit, str):
            raise TypeError("Snapshot 'git_commit' must be a string or None.")

libspec.common.Implemented dataclass

Source code in libspec/common.py
@dataclass(frozen=True)
class Implemented:
    ref: str
    spec_hash: str
    file: str
    line: int
    session_id: str | None = None

    def __post_init__(self):
        if not isinstance(self.ref, str) or not self.ref.strip():
            raise ValueError("Implemented 'ref' must be a non-empty string.")
        if not isinstance(self.spec_hash, str) or len(self.spec_hash) != 64:
            raise ValueError(
                "Implemented 'spec_hash' must be a 64-character SHA-256 hash string."
            )
        if not isinstance(self.file, str) or not self.file.strip():
            raise ValueError("Implemented 'file' must be a non-empty string.")
        if not isinstance(self.line, int) or self.line <= 0:
            raise ValueError("Implemented 'line' must be a positive integer.")
        if self.session_id is not None and not isinstance(self.session_id, str):
            raise TypeError("Implemented 'session_id' must be a string or None.")