Skip to content

symfonic.platform.core_preference_policy

core_preference_policy

Explicit, per-app enforcement of declared core preferences at egress.

The legacy critic reviewed every draft against DomainTemplate.core_preferences without the app asking for it. That implicit behaviour is deliberately not restored. An app that wants its preferences enforced says so here, supplies the reviewer that judges a draft against them, and gets a native governance capability whose egress review is consulted on every governed turn.

The defaults are explicit too:

  • enforce=False (the default) composes exactly what the migration already gave an app -- the sensitive-term watch -- and decides nothing about the preference text. Supplying a reviewer without declaring enforcement is refused rather than guessed at.
  • enforce=True with no reviewer, with no preferences, or with a reviewer that has no review method is refused at composition. An app that declared enforcement never serves a turn that is not enforced.
  • A reviewer that raises, or returns something other than a Reflection, asks for a revision. The stage underneath fails open on error, so the fail-closed answer has to be produced here.

The preferences are bound at composition, per app. Nothing is read from a tenant, and the adapter holds no per-scope state. The reviewer receives the turn's governance context; an app needing tenant-specific policy must bind that scope through its own reviewer rather than assuming a scope path is set.

CorePreferencePolicyError

Bases: PlatformError

The app's declared preference enforcement cannot be composed.

PreferenceReviewer

Bases: Protocol

Judges a draft against the app's declared preferences.

core_preference_governance

core_preference_governance(migration: CorePreferenceMigration, *, enforce: bool = False, reviewer: PreferenceReviewer | None = None, **ports: Any) -> GovernanceStages

Build the governance capability for an app's declared preferences.

build_agent(governance_capability=core_preference_governance(
    core_preference_migration(PREFERENCES),
    enforce=True,
    reviewer=MyReviewer(),
))

Remaining keywords go to :func:~symfonic.platform.governance. Enforcement owns the egress reviewer and its trigger, so reflector and trigger_policy are refused alongside it. Caller sensitive_terms extend, never replace, the migrated preference tags.

Source code in src/symfonic/platform/core_preference_policy.py
def core_preference_governance(
    migration: CorePreferenceMigration,
    *,
    enforce: bool = False,
    reviewer: PreferenceReviewer | None = None,
    **ports: Any,
) -> GovernanceStages:
    """Build the governance capability for an app's declared preferences.

        build_agent(governance_capability=core_preference_governance(
            core_preference_migration(PREFERENCES),
            enforce=True,
            reviewer=MyReviewer(),
        ))

    Remaining keywords go to :func:`~symfonic.platform.governance`.  Enforcement
    owns the egress reviewer and its trigger, so ``reflector`` and
    ``trigger_policy`` are refused alongside it. Caller ``sensitive_terms``
    extend, never replace, the migrated preference tags.
    """
    if not enforce:
        if reviewer is not None:
            raise CorePreferencePolicyError(
                "a preference reviewer was supplied without enforce=True; "
                "declare enforcement or drop the reviewer"
            )
        return governance(sensitive_terms=_combined_sensitive_terms(migration, ports), **ports)
    review = getattr(reviewer, "review", None)
    if not callable(review) or not inspect.iscoroutinefunction(review):
        raise CorePreferencePolicyError(
            "enforce=True requires a reviewer with an async review method"
        )
    if not migration.guardrails:
        raise CorePreferencePolicyError("enforce=True declared with no core preferences")
    clash = sorted({"reflector", "trigger_policy"} & ports.keys())
    if clash:
        raise CorePreferencePolicyError(
            f"enforce=True owns the egress review; remove {', '.join(clash)}"
        )
    return governance(
        sensitive_terms=_combined_sensitive_terms(migration, ports), **ports,
        reflector=_PreferenceReflector(reviewer, migration.guardrails),
        trigger_policy=_EveryTurn(),
    )

critic_preference_reviewer

critic_preference_reviewer(provider: Any, *, model_config: Any = None, prompt_template: str | None = None) -> PreferenceReviewer

Build the ported critic as an enforcement reviewer.

core_preference_governance(
    migration,
    enforce=True,
    reviewer=critic_preference_reviewer(provider),
)

provider answers the hidden reflection call (get_chat_model + ainvoke); model_config passes through to get_chat_model. prompt_template overrides the critic's stock prompt, with the same {draft}, {core_preferences}, {procedural_rules} and {sensitive_tag_hit} slots; None keeps the stock text.

Source code in src/symfonic/platform/core_preference_policy.py
def critic_preference_reviewer(
    provider: Any,
    *,
    model_config: Any = None,
    prompt_template: str | None = None,
) -> PreferenceReviewer:
    """Build the ported critic as an enforcement ``reviewer``.

        core_preference_governance(
            migration,
            enforce=True,
            reviewer=critic_preference_reviewer(provider),
        )

    ``provider`` answers the hidden reflection call (``get_chat_model`` +
    ``ainvoke``); ``model_config`` passes through to ``get_chat_model``.
    ``prompt_template`` overrides the critic's stock prompt, with the same
    ``{draft}``, ``{core_preferences}``, ``{procedural_rules}`` and
    ``{sensitive_tag_hit}`` slots; ``None`` keeps the stock text.
    """
    return _CriticPreferenceReviewer(provider, model_config, prompt_template)