corespine
Reference

Extending

A how-to for consumers and third parties — add a seam implementation, wire a real backend behind an optional extra, or bind your own conformance invariants without touching corespine.

本文是操作指南:第三方包如何在不改 corespine 一行代码的前提下,给某条缝补一个实现、 接一个真实后端、绑自己的不变量。机制源码见 seam/registry.pyconformance/harness.py;宪章见 CLAUDE.md,增长判据见 roadmap,现状见 prd

corespine 只提供机制,扩展点全在缝上,四种方式互不耦合:

扩展方式解决什么关键 API章节
entry-point 扩展一条缝第三方装包即被 make 发现,无需改核心Registry.make / group corespine.<seam>
可选 extra + 延迟 import真实后端 SDK 只在选用时才 import,缺依赖给友好提示lazy_extra_import / [project.optional-dependencies]
给一条缝绑 conformance 不变量app 用自己的不变量逮住坏实现InvariantPack / ConformanceSuite
用 trace 缝做可观测 + 接导出后端步骤发非敏感元数据;经 TraceExporter 扇出,后端(OTel 等)走 app/contrib 侧TraceSink / TraceExporter / InProcessPrivacyTraceSink / FORBIDDEN_KEYS

一、用 entry-point 扩展一条缝

每条缝是一个 Registry("<seam>") 实例;make(spec) 找不到内置名时,回落到 importlib.metadata 的 entry-point 自动发现,group 命名固定为 corespine.<seam>。 于是第三方只需在自己的 pyproject.toml 声明一个 entry point,装包即扩展,corespine 核心一行不改。

第三方包的 pyproject 片段

假设某条缝叫 vector_store,你的包 myadapter 想注册一个名为 pgvector 的实现:

# myadapter/pyproject.toml
[project.entry-points."corespine.vector_store"]
# 名字(左) -> "模块:工厂可调用对象"(右);工厂签名为 (**kwargs) -> 实现实例
pgvector = "myadapter.pg:make_pgvector"
# myadapter/pg.py
def make_pgvector(**kwargs):
    """工厂:返回一个实现该缝 Protocol 的实例(此处省略真实细节)。"""
    return PgVectorStore(**kwargs)

装包后的调用示例

from corespine import Registry

# 同一条缝名 "vector_store" 必须与 entry-point group 后缀一致。
registry: Registry = Registry("vector_store")

# 内置名找不到 → 自动扫 group "corespine.vector_store" 发现 myadapter 的 pgvector。
store = registry.make("pgvector", dsn="postgresql://...")

# 解析大小写 / 连字符 / 留白不敏感:"PG-Vector" / " pgvector " 都解析到同一项。
registry.names()  # 列出【内置 + entry-point 发现】的全部可用名(字典序、去重)

要点:

  • group 名必须corespine.<seam>,<seam>Registry("<seam>") 的入参完全一致;
  • 内置注册优先于 entry-point:同名时内置胜出(便于在测试里覆盖);
  • 发现是延迟的:只在 make / names 解析时才扫,不在 import 期付出代价;
  • 未知 spec 抛 ValueError,并列清当前全部可用名(绝不让人猜)。

已出货的真实范例: 0.2.0 的 blob 缝就是这套模式的落地——BLOB_REGISTRY = Registry("blob") 内置 memory / filesystem 两个离线默认,真实后端(S3 / MinIO)经 group corespine.blob 的 entry-point + lazy_extra_import 延迟接入,corespine 的 dependencies 仍为空。第三方接自己的缝时 照抄这个形状即可。

二、可选 extra 命名约定

薄核宪章(ADR 0001 D5):核心 dependencies 永远为空,默认路径零重依赖。真实后端的 SDK(Redis / OpenAI / …)由各 app 在自己的缝里经可选 extra 声明,并用 lazy_extra_import 延迟 import —— 选用该 adapter 时才 import,没装就给出可直接照做的安装指引。

extra 命名约定

约定示例说明
一个真实后端 → 一个 extra,以后端命名[redis] / [openai]名字即"装哪个后端",直观可猜
extra 内只放该后端的 SDKredis = ["redis>=5"]不夹带无关依赖,装得最小
lazy_extra_importextra= 与之同名extra="redis"缺依赖时提示 pip install <pkg>[redis] 即可修
# 你的包 pyproject.toml:真实后端依赖声明为可选 extra(以后端命名)
[project.optional-dependencies]
redis = ["redis>=5"]
openai = ["openai>=1.0"]

lazy_extra_import 用法

lazy_extra_import(module, *, pkg, extra) 把裸 ImportError 翻译成 pip install <pkg>[<extra>] 的友好提示:

from corespine import lazy_extra_import


def make_redis_queue(**kwargs):
    # 只在真正构造该 adapter 时才 import;没装 redis 不影响核心离线默认路径。
    redis = lazy_extra_import("redis", pkg="myadapter", extra="redis")
    client = redis.Redis(**kwargs)
    return RedisQueue(client)

未装 redis 时调用 make_redis_queue(...) 会抛:

ImportError: 缺少可选依赖 'redis':请先 `pip install myadapter[redis]` 再重试。

—— 而不是让调用方对着 ModuleNotFoundError: No module named 'redis' 自己猜该装哪个 extra。

三、给一条缝绑 conformance 不变量

corespine 的 conformance 是机制,非保证:harness 只负责"跑 实现 × 不变量 的笛卡尔积 + 报告哪个格子坏了",具体不变量由各 app 自己绑(ADR 0001 D6)。app 把自己的 InvariantPack 喂进 ConformanceSuite 即可。

最小骨架(完整可跑范例见 examples/conformance_usage.py):

from corespine import ConformanceSuite, InvariantPack

# 1) 实现注册表:名字 -> 无参工厂(每格各新建实例,杜绝实现间状态串味)
impls = {"counter": Counter, "broken": BrokenCounter}

# 2) app 自己的不变量包(corespine 核心不含任何具体不变量)
pack = (
    InvariantPack("counter-contract")
    .add("first-add-returns-n", lambda c: assert_eq(c.add(3), 3))
    .add("accumulates", lambda c: assert_eq((c.add(2), c.add(5)), (2, 7)))
)

# 3) 绑成笛卡尔积,逐格跑
suite = ConformanceSuite(impls, pack)
for impl, invariant in suite.cases():
    suite.check(impl, invariant)   # 失败即抛,定位到具体格子
# 或:suite.run() 收集全部结果(不抛);suite.passed() 便捷判全过

要点:

  • 不变量 = (实现实例) -> None,通过则正常返回、违反则抛异常;只验外部可观测行为;
  • 每个格子都新建实例(工厂无参),杜绝实现间状态串味;
  • cases() 返回 (实现名, 不变量名) 列表,可直接喂 pytest.mark.parametrize; ids() 给对齐的可读 id(形如 impl/invariant);
  • 这正是"敢放手让第三方填广度、却让脊柱不变量烂不掉"的落点:没过 conformance 的实现直接 CI 红,而非生产事故。

四、用 trace 缝做可观测

trace 缝(机制源码 observability/trace.py)只给三样 机制:TraceSink 协议(出口的最小结构面 emit(code, **fields))、InProcessPrivacyTraceSink 默认实现(隐私 by construction:载荷命中 FORBIDDEN_KEYS 即抛 TraceError、绝不记录)、以及 FORBIDDEN_KEYS 这份禁词键集合。具体记什么 code、绑什么不变量,由各 app 自己定

完整可跑范例见 examples/trace_seam_usage.py,演示两种真实消费形态:

形态 1:隐私闸门(把默认 sink 包成自家薄封装)

InProcessPrivacyTraceSink 当作落盘前的强制隐私闸门:每条 trace 先过禁词键校验,过闸后才 落自家渠道(日志 / DB)。命中正文字段即抛,正文绝不外泄。

from corespine import InProcessPrivacyTraceSink, TraceError

class PrivacyGatedTrace:
    def __init__(self) -> None:
        self._gate = InProcessPrivacyTraceSink()

    def emit_trace(self, code: str, **fields: object) -> None:
        self._gate.emit(code, **fields)   # 命中 FORBIDDEN_KEYS 直接抛,不会落到下游
        ...                               # 过闸后才落自家日志 / DB

形态 2:注入点(以 TraceSink 形参接收任意 sink)

让步骤/方法以 trace: TraceSink | None 形参接收 sink,关键处只 emit(code, **非敏感字段); 塞哪个 sink 由 host 决定。注意只发长度 / 计数 / 耗时 / 标志,绝不发正文。

from corespine import TraceSink

def run_step(task: str, *, trace: TraceSink | None = None) -> str:
    output = do_work(task)
    if trace is not None:
        trace.emit("step", task_chars=len(task), output_chars=len(output))  # 只发长度
    return output

接真实导出后端(OTel 等):经 TraceExporter 扇出,一律走 app/contrib 侧

0.2.0 起,把 trace 扇出到进程外有了专用缝:TraceExporter 协议(export(event: TraceEvent) -> None)

  • 进程内默认 InProcessTraceExporterInProcessPrivacyTraceSink 可选挂 0..N 个 exporter (构造传 exporters=[...] 或事后 add_exporter(...)),emit 的合法事件在隐私校验之后逐个扇出。

隐私不变量(不可放松): exporter 只会收到已过 FORBIDDEN_KEYS 校验的 TraceEvent——受限 正文在 emit 的校验阶段就被 TraceError 挡下,根本到不了任何 exporter。故导出面与本地记录面 等宽,天然只承载 code / 计数 / 耗时。挂 exporter 不会让隐私约定漏气。

from corespine import InProcessPrivacyTraceSink, InProcessTraceExporter, TraceExporter, lazy_extra_import

class OTelTraceExporter:                    # 在【你的包】里,不在 corespine 核心
    def __init__(self) -> None:
        # OTel SDK 经你自己的可选 extra 声明,延迟到真正构造时才 import。
        self._otel = lazy_extra_import("opentelemetry.trace", pkg="myadapter", extra="otel")
        ...
    def export(self, event) -> None:        # 只会收到已过隐私校验的 TraceEvent(无需再自查禁词键)
        ...                                 # 建 span / 记 metric:event.code / event.fields(只含元数据)

# host 组装:合法事件先本地记录、再扇出到你的 OTel exporter
sink = InProcessPrivacyTraceSink(exporters=[OTelTraceExporter()])
# 或注入点里的 sink 也能挂:sink.add_exporter(OTelTraceExporter())

铁律(ADR 0001 D5 / 宪章): corespine 的 dependencies 永远为空,核心绝不 import 任何 导出后端 SDK(OTel / 日志聚合 / APM)。核心只带 TraceExporter 协议 + InProcessTraceExporter 进程内收集器;真实导出后端只在各 app 的可选 extra 或 contrib 里实现,默认路径恒为离线确定性。

要点:

  • trace 只发非敏感元数据(code / 计数 / 耗时 / 标志);禁词键集合见 FORBIDDEN_KEYS;
  • 扇出经 TraceExporter 缝,严格发生在隐私校验之后——exporter 天然收不到正文,无需自己再兜隐私;
  • 默认 exporter(InProcessTraceExporter)离线确定性、零依赖;真实导出后端永远在 app/contrib 侧, 核心不依赖任何 SDK;
  • 若你更想在落盘前就拦截(而非扇出),仍可用形态 1 的隐私闸门模式;两者互补。

On this page