RFCs

RFC 1502: Scope Source Discovery

概要

扩展 GET /v1/scopes,提供 SQL 层的 Scope 发现和兼容分页。调用方通过 query 提供子串,并通过 query_field 明确指定一个搜索字段;可选精确过滤参数覆盖父 Scope、External Reference kind 以及 Binding integration/kind。新增 GET /v1/scopes/{scope_id}/sources,分页读取一个 Scope 持有的公开 Source。

Scope discovery 可以选择 scope_idtitlesummary、External Reference value 或 Binding external_id。 服务端持久化由应用统一规范化的搜索投影,再使用数据库方言对应的字面包含表达式,保证 SQLite 与 OceanBase 具有相同的 Unicode、大小写和特殊字符行为。

动机

ScopeApplication 当前生成不透明的 scp_... ID。调用方通常知道标题、仓库 URL 或工作区 Binding,而不是 随机 ID 的片段,因此只搜索 scope_id 无法通过仓库信息发现新 Scope。

让一个 query 隐式搜索全部字段也不合适:调用方无法判断命中原因,新增搜索字段会改变已有请求结果,关系 字段的授权与查询成本也不同。要求 query_field 可以稳定请求语义和 SQL 执行范围,同时保留统一集合接口。

选择 Scope 后,调用方能够创建 Source 或按完整身份读取 Source,但无法发现该 Scope 已有的 Source。Source 发现是读取既有子资源集合,因此在 /v1/scopes/{scope_id}/sources 上增加 GET。

使用说明

发现 Scope

假设一个不透明 Scope 的标题为 PowerContext,并具有 value 为 https://github.com/oceanbase/powercontextrepository External Reference。按标题搜索:

GET /v1/scopes?query=powercontext&query_field=title&limit=50
{
  "items": [
    {
      "scope_id": "scp_01hzy8m6yq8j3h7m3v5w2r9k1p",
      "title": "PowerContext",
      "summary": "PowerContext repository development",
      "parent_scope_id": null,
      "context_references": [],
      "external_references": [
        {
          "kind": "repository",
          "value": "https://github.com/oceanbase/powercontext"
        }
      ],
      "version": 1
    }
  ],
  "next_cursor": null
}

也可以明确搜索仓库引用:

GET /v1/scopes?query=oceanbase%2Fpowercontext&query_field=external_reference_value&external_reference_kind=repository&limit=20

所有过滤条件按 AND 组合,query 只匹配 query_field 指定的一个字段。目标是 External Reference 或 Binding 时,文本条件和精确类型条件必须由同一条关系记录满足。

无参数 GET /v1/scopes 保留现有全量列表行为。提供搜索、关系过滤、limitcursor 后进入分页模式。 next_cursor 非空时,调用方将其原样传入下一次请求,并保持相同 query、field、过滤参数和 limit。

遍历 Scope 的公开 Source

GET /v1/scopes/scp_01hzy8m6yq8j3h7m3v5w2r9k1p/sources?limit=50
{
  "items": [
    {
      "scope_id": "scp_01hzy8m6yq8j3h7m3v5w2r9k1p",
      "source_type": "content",
      "source_id": "src_example_0001",
      "content": {"decision": "Keep the public API stable."},
      "position": 1,
      "content_digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
    }
  ],
  "next_cursor": null
}

每个 item 复用精确 Source Get 的 SourceRecord,包括适用的 receipt_identity 等可选字段。集合只包含公开 content Source,包括可读取的 lineage_only Source;不公开内部类型,也不带入 Parent、Context Reference 或 Subject Scope 中的 Source。

参考级说明

Scope discovery 合同

参数类型必填规则示例
querystring条件必填query_field 同时提供;最长 256 个 Unicode 字符powercontext
query_fieldstring enum条件必填与非空 query 同时提供;一次选择一个字段title
parent_scope_idstring精确匹配直接父 Scope;不递归scp_01hzy8m6yq8j3h7m3v5w2r9k1p
external_reference_kindstringScope 存在该 kind 的 External Referencerepository
binding_integrationstringScope 存在该 integration 的 Bindingcodex
binding_kindstringScope 存在该 kind 的 Bindingworkspace
limitinteger分页默认 50,范围 1–10050
cursorstring首次省略;随后传服务端不透明值eyJ2ZXJzaW9uIjoxLC4uLn0

query_field 是封闭枚举:

  • scope_id
  • title
  • summary
  • external_reference_value
  • binding_external_id

queryquery_field 必须成对出现。缺少任一参数、枚举值未知、参数重复,或 query_field 配合空/纯空白 query,均返回 422。本期不提供 any,也不支持多字段 OR 搜索。

服务端去除 query 首尾空白,再执行 NFKC 和 Unicode casefold 后的连续字面子串匹配;不分词、不纠错、不做 语义搜索。%_*、反斜线和正则字符均按普通字符处理。

文本匹配与精确过滤按 AND 组合。External Reference 和 Binding 使用相关 EXISTS,避免一对多关系产生重复 Scope。query_field=external_reference_value 且提供 external_reference_kind 时,同一条 External Reference 必须同时满足 value 和 kind;Binding 文本与 integration/kind 同理。

Scope 分页与兼容性

Scope 按 scope_id 升序执行 keyset 分页。无任何 query 参数的 GET /v1/scopes 保留现有无界结果;提供有效 query pair、精确过滤、limitcursor 时进入分页模式,默认 50、最大 100。

ScopePage 增加可选 next_cursor: string|null。分页响应总是返回该字段;旧全量响应可以省略或返回 null。 签名 cursor 绑定 operation、调用方稳定身份、规范化 query、query field、精确过滤、limit、排序、最后一个 Scope ID 和过期时间,复用部署的 cursor secret 与 record-cursor TTL。cursor 不能跨调用方、字段、过滤条件、 limit 或接口使用;不合法返回 400 invalid_cursor,过期返回 410 cursor_expired

Scope ID 随机生成且可搜索元数据可更新。本期提供 keyset consistency,不提供跨请求快照。数据与权限不变时, 匹配 Scope 恰好出现一次;并发创建或元数据更新可能改变后续页,需要最新目录的调用方应从第一页重新开始。

SQL 实现与存储兼容

Scope repository 接收 query pair、精确过滤、keyset 边界和 limit + 1。过滤、排序和限制必须在 SQL 中完成, 不能先加载无界 Scope 列表再由 Python 过滤或分页。

应用定义唯一规范化函数:NFKC 后执行 Unicode casefold(),不分词、不删除标点。Scope、External Reference 和 Binding 写入路径持久化规范化结果,启动 migration 对历史记录回填:

原字段搜索投影
pc_scopesscope_idscope_id_search
pc_scopestitletitle_search
pc_scopessummarysummary_search
pc_scope_external_referencesvaluevalue_search
pc_scope_bindingsexternal_idexternal_id_search

原字段仍是事实来源,投影不进入公开响应。由应用生成投影可以避免数据库 generated column、LOWER() 和默认 collation 的差异。

SQL 方言 helper 对 SQLite 生成 instr(normalized_column, :query) > 0,对 OceanBase/MySQL 模式生成 locate(:query, normalized_column) > 0,两者都把 SQL 通配符当作普通字符。query 始终使用绑定参数。如果最终 使用 LIKE,必须由公共 helper 统一转义,并在两种存储合同测试中证明等价。

Repository 只构造 query_field 选择的一个谓词,不生成覆盖全部投影的固定 OR。主表字段使用直接条件,关系 字段使用相关 EXISTS,类型限制进入同一个子查询。选出一页 Scope 后,复用现有批量 loader 装配 Context References 和 External References,不能产生 N+1。

字面包含通常无法使用普通 B-tree 前缀索引。SQL 下推仍能限制数据库到应用的传输和应用内存。只有查询计划 证明需要时,才增加以 scope_id 开头的跨存储 Binding 索引。

Source List 合同

字段
方法和路径GET /v1/scopes/{scope_id}/sources
operationIdlist_sources
鉴权既有 path_scope_read_access / scope.read
响应SourcePage

Query 参数为可选 limit(默认 50,范围 1–100)和不透明签名 cursorSourcePage 包含必填的 items: SourceRecord[]next_cursor: string|null。已存在 Scope 没有公开 Source 时返回空数组和 null; Scope 不存在或不可见时沿用 403/404 策略。

只有已建立公开读取合同的 Source 类型进入集合;本期与精确 Get 一致,仅包含 content。列出 lineage_only Source 不会赋予生成资格。

Source 按 journal position 升序返回。第一页记录已提交高水位,后续页读取 last_position < journal_position <= high_watermark,新追加 Source 在新一轮遍历中可见。cursor 绑定 operation、 Scope、公开类型、排序、limit、调用方、高水位、最后位置和过期时间。

每页最多返回 limit 条,普通页 UTF-8 JSON 内容预算为 4 MiB。服务端不截断 Source;第一条超过预算时,可以在 既有 Source 大小限制内完整返回这一条。

授权与错误

Scope discovery 保留 server.observe,授权完成后才搜索元数据或 Binding,响应只包含 ScopeDescriptor。Source List 保留 scope.read 和精确 Get 的可见性规则。每页重新鉴权,cursor 不能绕过授权变化。

状态码含义
200成功,包括合法空集合
400cursor 不合法、被篡改或上下文不匹配
401缺少或无效凭证
403/404既有授权和资源可见性策略
410cursor 过期
422query/field 未配对、过滤、limit、path 或重复参数不合法
503必要持久化或运行时能力不可用
500使用既有错误 envelope 的未预期错误

OpenAPI 与客户端

openapi/powercontext.yaml 继续是事实来源。list_scopes 增加可选 queryquery_fieldparent_scope_idexternal_reference_kindbinding_integrationbinding_kindlimitcursorquery_field 使用封闭枚举,请求模型执行与 query 的配对校验。ScopePage 增加可选 next_cursor;同时定义 list_sourcesListSourcesRequestSourcePage

重新生成 Python/TypeScript bindings;手写 Python client 保留无参 list_scopes() 并暴露 discovery 参数。 接口不写 Source、不推进 consumer cursor、不调用模型,也不创建 Artifact 或 Candidate。

缺点

规范化投影及启动 migration 增加存储和写入维护成本;字面包含仍可能扫描记录。旧无参 Scope 列表仍然无界, Scope keyset 分页不是跨请求快照。返回完整 Source content 比摘要集合开销更大。

设计选择与替代方案

  • 不固定只搜索 scope_id:当前不透明 ID 不承载仓库身份。
  • 不让一个 query 默认搜索全部字段:命中来源和已有请求行为不稳定。
  • 不为每个字段定义独立 query 参数:参数面较大,多字段同时出现还需要额外 AND/OR 规则;当前单字段场景用 query + query_field 更清晰。
  • 不在 Python 中过滤:会加载全量集合,也无法正确下推分页。
  • 不依赖数据库 LOWER() 或默认 collation:SQLite 与 OceanBase 的 Unicode 行为不同。
  • 不直接 JOIN 关系表:会产生重复 Scope 并破坏分页边界。
  • 不增加独立 Scope Search:本需求过滤同一个授权集合,并复用同一响应模型。

既有设计

RFC 1437 定义 Source 身份、公开 Content Source、精确 Get 和 lineage_only 语义。本文增加 Scope 与 Source 集合发现,不改变这些语义。

未决问题

本期没有未决问题。跨字段相关性、拼写纠错、全文索引和返回 Binding 身份需要独立设计及更细授权评审。

未来可能

未来可以迁移旧无界 Scope 列表,增加根/子树或更新时间过滤、External Reference value 精确匹配,或者引入 专用全文索引。任何扩展都必须保持 cursor 上下文、授权以及 SQLite/OceanBase 语义一致。

On this page