Skip to content

symfonic.cli.public_api

public_api

What a scaffolded project is allowed to import from symfonic (T4.3.1).

REQ-S4.3: CLI and scaffolds generate only public APIs and target architecture patterns. A generated project is the one piece of code the framework writes on an adopter's behalf, so every symfonic import it emits is a promise the framework then has to keep. This module is where that promise is written down.

Two authorities decide what "public" means; this module invents no third.

Declared public — T1.1.1's public-surface-inventory.json: a package is public when it publishes __all__, and a name is public when it is in one. Membership is resolved against the live package at call time rather than copied, so a package that stops declaring a name fails the check instead of drifting away from a transcribed list.

Sanctioned undeclared — T1.1.2's uncatalogued-triage.md: several clusters are documented, tested, and adopter-imported while declaring nothing. Triage gave each a verdict. A cluster marked (a) promote or (b) compat-alias is public in substance and undeclared only in form, so the scaffold may use it through a :data:SANCTIONED_UNDECLARED row naming that verdict. A cluster with no such verdict gets an :data:OPEN_GAPS row instead — pinned, not permitted.

The distinction that makes this worth enforcing: an undeclared path with no verdict is not a style problem. It is a decision the framework has not made, and shipping it inside a generated project makes that decision for them, silently, in whichever direction the next refactor happens to move the module.

OPEN_GAPS module-attribute

OPEN_GAPS: dict[str, OpenGap] = {}

Empty as of the kernel-native scaffold migration.

All four rows closed the same way, and it is worth recording which way, because none of them closed by declaring the module the gap named.

  • PUB-4 (CorePreference) -- the generated domain no longer fills a DomainTemplate. It declares a DomainPersona and a list of Guardrail, both public on symfonic.capabilities.prompting, so the half-public modelling API is not reached at all.
  • PUB-5 (ConversationMetricsCollector) and PUB-7 (PostgresBudgetStore) -- both are now process resources the host owns and hands over, through symfonic.platform.observability. The scaffold names the bundle, not the internals.
  • PUB-6 (the metrics store's factory and its module globals) -- triaged '(c) internalize store internals', and that verdict stands: the internals stayed internal and the generated app reads the store off the bundle instead of a process global.

A new private dependency must be added back here deliberately; the suite asserts this set is neither exceeded nor stale.

OpenGap dataclass

OpenGap(finding: str, names: frozenset[str], triage: str, consequence: str)

A symbol the scaffold needs that the framework has not declared public.

Distinct from :class:UndeclaredException in the way that matters: an exception carries a T1.1.2 verdict saying the symbol is public and merely undeclared, so the debt is paperwork. A gap has no such verdict — or has one pointing the other way — so the scaffold depends on a surface nobody promised to keep.

Gaps are pinned rather than fixed because closing one means editing a package this task holds no write authority over (T1.2.7 gives T4.3.1 src/symfonic/cli/** and its evidence directory). Pinning makes the set non-increasing, which is the strongest claim a task can honestly make from inside its own boundary.

consequence instance-attribute

consequence: str

What breaks for an adopter if the symbol moves or is internalised.

triage instance-attribute

triage: str

T1.1.2's verdict for the cluster, or untriaged for the long tail.

PublicApiVerdict dataclass

PublicApiVerdict(public: bool, reason: str = '', exception: UndeclaredException | None = None)

Whether an import may be emitted, and the sentence explaining why not.

UndeclaredException dataclass

UndeclaredException(triage: str, finding: str, names: frozenset[str], reason: str)

One T1.1.2 cluster the scaffold may import before it is declared.

finding instance-attribute

finding: str

This task's finding id (PUB-n) tracking the declaration work.

names instance-attribute

names: frozenset[str]

The exact symbols sanctioned. Sanctioning the module would re-admit every future symbol in it without anyone deciding.

triage instance-attribute

triage: str

a-promote or b-compat-alias — the T1.1.2 verdict, verbatim.

classify_import

classify_import(ref: ImportRef) -> PublicApiVerdict

Decide whether ref is a public import a scaffold may emit.

Pinned :data:OPEN_GAPS are deliberately not consulted here: a gap is still a private import, and a classifier that called it public would make the pinning a way of passing the test rather than a way of recording a debt. The suite compares the gap set separately.

Source code in src/symfonic/cli/public_api.py
def classify_import(ref: ImportRef) -> PublicApiVerdict:
    """Decide whether *ref* is a public import a scaffold may emit.

    Pinned :data:`OPEN_GAPS` are deliberately **not** consulted here: a gap is
    still a private import, and a classifier that called it public would make
    the pinning a way of passing the test rather than a way of recording a
    debt. The suite compares the gap set separately.
    """
    sanctioned = SANCTIONED_UNDECLARED.get(ref.module)
    if sanctioned is not None:
        if ref.name is None or ref.name in sanctioned.names:
            return PublicApiVerdict(True, sanctioned.reason, sanctioned)
        return PublicApiVerdict(
            False,
            f"{ref.module} is sanctioned only for {sorted(sanctioned.names)}; "
            f"{ref.name!r} is not in that row ({sanctioned.finding})",
        )

    if ref.module not in PUBLIC_PACKAGES:
        return PublicApiVerdict(
            False,
            f"{ref.module} declares no public surface and has no T1.1.2 "
            "(a)-promote row; import through the declared package instead",
        )

    if ref.name is None:
        return PublicApiVerdict(True, f"{ref.module} is a declared public package")

    if ref.name in _declared(ref.module):
        return PublicApiVerdict(True, f"{ref.module}.__all__ declares {ref.name}")

    return PublicApiVerdict(
        False,
        f"{ref.module} is public but its __all__ does not declare {ref.name!r}; "
        "the name is undeclared surface with no sanctioned row",
    )

open_gap_for

open_gap_for(ref: ImportRef) -> OpenGap | None

Return the pinned gap covering ref, or None when it is not one.

Source code in src/symfonic/cli/public_api.py
def open_gap_for(ref: ImportRef) -> OpenGap | None:
    """Return the pinned gap covering *ref*, or ``None`` when it is not one."""
    gap = OPEN_GAPS.get(ref.module)
    if gap is None or (ref.name is not None and ref.name not in gap.names):
        return None
    return gap