diff --git a/research-wiki/designs/2026-08-17-issue11-caller-dimensions-design.md b/research-wiki/designs/2026-08-17-issue11-caller-dimensions-design.md index 418988d..fcf8fc0 100644 --- a/research-wiki/designs/2026-08-17-issue11-caller-dimensions-design.md +++ b/research-wiki/designs/2026-08-17-issue11-caller-dimensions-design.md @@ -1,6 +1,6 @@ # 调用方自定义维度设计(issue #11) -- **状态**: 待 Codex 审 → 待人类审批 +- **状态**: **已人类审批(2026-08-17)**;Codex 审查 4 项已全部采纳并修订 - **触发**: issue #11「遥测表 llm_calls 缺少租户维度,多租户调用方无法在数据库层隔离」 - **范围**: 公共 API(`chat`/`embed` 签名)、`ports.TelemetryRecorder` 端口、`types.ChatRequest`、两个遥测后端的 schema。属 CLAUDE.md §3 强制设计档 + 人类门。 diff --git a/research-wiki/designs/issue11-caller-dimensions.md b/research-wiki/designs/issue11-caller-dimensions.md index 436e529..6640de8 100644 --- a/research-wiki/designs/issue11-caller-dimensions.md +++ b/research-wiki/designs/issue11-caller-dimensions.md @@ -7,7 +7,9 @@ date: 2026-08-17 # 调用方自定义维度设计(issue #11) -正文: `2026-08-17-issue11-caller-dimensions-design.md`。状态: 待 Codex 审 → 待人类审批。 +正文: `2026-08-17-issue11-caller-dimensions-design.md`。状态: **已人类审批(2026-08-17)**,进入 writing-plans。 + +审批时三个待定项按设计原值定稿,人类未提出改动: `meta` key 数量上限 16 / `str` value 上限 256 / `tenant_id` 上限 128(量级推断,无本项目实测依据);库不自动建索引、不自动启用 RLS(GovDoc 需 DBA 执行模板 SQL 才拿到数据库层隔离);`_BACKFILL` 自动 ALTER 降级议题**不纳入本次**。 - **选定方案**: `tenant_id` 提真实列(RLS 硬需求)+ `meta` JSON 容器承载任意调用方自定义 KV(**默认不建索引**)。`chat()`/`embed()` 各增两个带默认值的 keyword-only 参数,签名冻结承诺不破。端口 22 → 24 字段。 - **范围收窄(2026-08-17 人类决策)**: 只做**调用方自定义**的维度;请求自带信息(模型名/供应商/源名)继续走现有列,库不往 `meta` 写任何自采信息。issue 第 4 条(保留期与访问控制)另开。 diff --git a/research-wiki/graph/edges.json b/research-wiki/graph/edges.json index 871ad31..9a8d13b 100644 --- a/research-wiki/graph/edges.json +++ b/research-wiki/graph/edges.json @@ -160,6 +160,11 @@ "id": "plan:issue10-error-body-retention-plan", "label": "实现计划: HTTP 错误响应体留存(Issue #10)", "type": "plan" + }, + { + "id": "plan:issue11-caller-dimensions", + "label": "调用方自定义维度实现计划(issue #11)", + "type": "plan" } ], "links": [ @@ -288,6 +293,13 @@ "relation": "implements", "evidence": "7 任务覆盖设计 G1-G4 与 §7 全部验收用例", "added": "2026-08-16T09:50:57.830855+00:00" + }, + { + "source": "plan:issue11-caller-dimensions", + "target": "design:issue11-caller-dimensions", + "relation": "implements", + "evidence": "按已批准设计拆解为 8 个任务,含设计范围外发现的 OCR 第三条链路", + "added": "2026-08-17T10:09:08.967997+00:00" } ] } \ No newline at end of file diff --git a/research-wiki/index.md b/research-wiki/index.md index aa3477b..8fc186d 100644 --- a/research-wiki/index.md +++ b/research-wiki/index.md @@ -1,6 +1,6 @@ # Research Wiki 索引 -> 自动生成,更新时间:2026-08-17 09:53 UTC +> 自动生成,更新时间:2026-08-17 10:09 UTC ## design (30) - [2026-07-20-m1-core-design](designs/2026-07-20-m1-core-design.md) `design:2026-07-20-m1-core-design` @@ -48,7 +48,7 @@ - [P7 OCR soak 验收: 99.73% 与 13 不变量全 PASS](findings/p7-ocr-soak.md) `finding:p7-ocr-soak` - [推理开关与 reasoning_tokens: 供应商实测与业界做法](findings/2026-08-02-thinking-switch-and-reasoning-tokens.md) `finding:2026-08-02-thinking-switch-and-reasoning-tokens` -## plan (23) +## plan (25) - [2026-07-20-m1-core-plan](plans/2026-07-20-m1-core-plan.md) `plan:2026-07-20-m1-core-plan` - [2026-07-20-m2-distributed-plan](plans/2026-07-20-m2-distributed-plan.md) `plan:2026-07-20-m2-distributed-plan` - [2026-07-21-m25-resilience-plan](plans/2026-07-21-m25-resilience-plan.md) `plan:2026-07-21-m25-resilience-plan` @@ -60,6 +60,7 @@ - [2026-08-06-governance-backend-error-plan](plans/2026-08-06-governance-backend-error-plan.md) `plan:2026-08-06-governance-backend-error-plan` - [2026-08-06-issue8-stall-budget](plans/2026-08-06-issue8-stall-budget.md) `plan:2026-08-06-issue8-stall-budget` - [2026-08-16-issue10-error-body-retention](plans/2026-08-16-issue10-error-body-retention.md) `plan:2026-08-16-issue10-error-body-retention` +- [2026-08-17-issue11-caller-dimensions](plans/2026-08-17-issue11-caller-dimensions.md) `plan:2026-08-17-issue11-caller-dimensions` - [est_tokens 解耦实施计划](plans/est-tokens-decoupling.md) `plan:est-tokens-decoupling` - [issue #8 实施计划: stall 非生产性等待口径](plans/issue8-stall-budget-plan.md) `plan:issue8-stall-budget-plan` - [M1 核心里程碑实现计划](plans/m1-core-plan.md) `plan:m1-core-plan` @@ -71,6 +72,7 @@ - [实现计划: HTTP 错误响应体留存(Issue #10)](plans/issue10-error-body-retention-plan.md) `plan:issue10-error-body-retention-plan` - [实现计划: 治理后端故障归位为 scope 级不可用(Issue #7)](plans/governance-backend-error.md) `plan:governance-backend-error` - [推理开关能力建模与 reasoning_tokens 采集实施计划(issue #5 + #6)](plans/2026-08-02-thinking-capability.md) `plan:2026-08-02-thinking-capability` +- [调用方自定义维度实现计划(issue #11)](plans/issue11-caller-dimensions.md) `plan:issue11-caller-dimensions` - [采样参数透传实现计划(issue #4)](plans/sampling-params-plan.md) `plan:sampling-params-plan` ## schema (1) diff --git a/research-wiki/log.md b/research-wiki/log.md index 589ec06..6e7d5d7 100644 --- a/research-wiki/log.md +++ b/research-wiki/log.md @@ -103,3 +103,6 @@ - [2026-08-16 09:50 UTC] 新增边: plan:issue10-error-body-retention-plan --implements--> design:issue10-error-body-retention - [2026-08-16 09:50 UTC] 重建索引: 66 篇页面 - [2026-08-17 09:53 UTC] 重建索引: 68 篇页面 +- [2026-08-17 10:09 UTC] 新增 plan: 调用方自定义维度实现计划(issue #11) (plan:issue11-caller-dimensions) +- [2026-08-17 10:09 UTC] 新增边: plan:issue11-caller-dimensions --implements--> design:issue11-caller-dimensions +- [2026-08-17 10:09 UTC] 重建索引: 70 篇页面 diff --git a/research-wiki/plans/2026-08-17-issue11-caller-dimensions.md b/research-wiki/plans/2026-08-17-issue11-caller-dimensions.md new file mode 100644 index 0000000..4952470 --- /dev/null +++ b/research-wiki/plans/2026-08-17-issue11-caller-dimensions.md @@ -0,0 +1,249 @@ +# 实现计划: 调用方自定义维度(issue #11) + +- **目标**: 让调用方能在每次调用上附带租户标识与任意自定义维度,并落进遥测表——`tenant_id` 为真实列(可挂 RLS),其余进 `meta` JSON 容器。 +- **方案概述**: `llm_calls` 增两列(`tenant_id TEXT NOT NULL DEFAULT ''`、`meta` JSON);三个公共入口(`chat`/`embed`/OCR 两方法)各增两个带默认值的 keyword-only 参数;校验在入口收口并抛裸 `ValueError`;`TelemetryRecorder` 端口 22 → 24 字段。库不建索引、不启用 RLS,只交付 policy 模板。 +- **依据设计**: `research-wiki/designs/2026-08-17-issue11-caller-dimensions-design.md`(已人类审批 2026-08-17)。 +- **涉及技术**: Python 3.11+、frozen dataclass、asyncpg、sqlite3、pytest。 +- **保真校验**: **本计划不涉及参考实现迁移,保真校验不适用**。 + +## 计划阶段的新发现(执行前需知) + +设计只覆盖 `chat()` 与 `embed()` 两条链路(issue 只诉求这两条)。写计划时核实代码发现**第三条链路**: `OcrClient` 同样经 `TelemetryEmitter.emit_attempt` 写遥测(`ocr.py:426`),其 `_emit` 在 `ocr.py:398` 现场构造 `ChatRequest`,结构与 embedding 完全同构。若不处理,OCR 行的 `tenant_id` 恒为空串,而 OCR 行与 chat 行落在**同一张表**——多租户下游的审计链会缺一块,且同样不可逆。 + +处置: 列为 **Task 6**,与 issue #10 的先例一致(那次 issue 只报告 chat 的 400,OCR 侧被认定为同一缺陷的其余分支而一并修)。此任务**超出已批准设计的字面范围**,执行者若被要求严格限定范围,可跳过 Task 6 并在 CHANGELOG 明确声明"OCR 行不带调用方维度"这一已知限制——但**不得静默跳过**。 + +--- + +## 文件结构 + +| 文件 | 动作 | 职责 | +|---|---|---| +| `src/polygateway/types.py` | 修改 | 新增 `validate_caller_dimensions()`;`ChatRequest` 增两字段 | +| `src/polygateway/ports.py` | 修改 | `TelemetryRecorder.record_llm_call` 22 → 24 字段 | +| `src/polygateway/telemetry/sqlite.py` | 修改 | DDL 增两列、`_BACKFILL_COLUMNS` 增两项、`_COLUMNS` 增两项 | +| `src/polygateway/telemetry/postgres.py` | 修改 | 同上(`_DDL`/`_BACKFILL`/`_COLUMNS`) | +| `src/polygateway/middleware/telemetry.py` | 修改 | `_record` 归一化并透传;三个 emit 入口从 request 读取 | +| `src/polygateway/client.py` | 修改 | `chat()` 增两参数并校验 | +| `src/polygateway/embedding.py` | 修改 | `embed()` 增两参数;沿 `_embed_batch`/`_attempt`/`_emit` 透传 | +| `src/polygateway/ocr.py` | 修改 | `recognize_text`/`parse_layout` 增两参数;沿 `_call`/`_attempt`/`_emit` 透传 | +| `tests/unit/test_types.py` | 修改 | 校验函数红线 | +| `tests/unit/test_telemetry.py` | 修改 | 三个 emit 入口带维度 | +| `tests/unit/test_client.py`、`test_embedding.py`、`test_ocr_client.py` | 修改 | 三条链路各自的端到端透传 | +| `tests/unit/test_ports.py` | 修改 | 端口 24 字段契约 | +| `tests/integration/test_postgres_telemetry.py` | 修改 | 真实 PG 补列与写入 | +| `README.md`、`CHANGELOG.md`、Gitea wiki | 修改 | 能力表、RLS 模板、版本说明 | + +**依赖顺序**: Task 1 → Task 2 → Task 3 → (Task 4 / 5 / 6 可并行) → Task 7 → Task 8。 + +--- + +## 关键接口(跨任务消费,此处定稿) + +校验函数(`types.py`,紧邻 `validate_request_overlay` 放置,同款风格): + +```python +def validate_caller_dimensions( + tenant_id: str | None, + meta: Mapping[str, Any] | None, + *, + origin: str, +) -> tuple[str | None, dict[str, Any]]: + """校验调用方维度并返回浅拷贝;origin 用于把错误指回调用点。""" +``` + +`ChatRequest` 新字段(必须带默认值,ARCH §5.1 约定①): + +```python +tenant_id: str | None = None +meta: Mapping[str, Any] = field(default_factory=dict) +``` + +`TelemetryRecorder.record_llm_call` 新增两个**无默认值**参数(`ports.py` 现有纪律),排在 `reasoning_tokens` 之后: + +```python +tenant_id: str, # 已归一化: None → '' +meta: str, # 已序列化: 空 dict → '{}' +``` + +**归一化在 emitter 完成,不在 recorder**——与 `sampling` 列由 `canonical_sampling_json()` 在 emitter 侧定型是同一先例。recorder 只负责落库,不做语义判断。 + +--- + +## Task 1: 校验函数与请求字段 + +**文件**: `src/polygateway/types.py`(修改)、`tests/unit/test_types.py`(修改) + +**行为**: + +在 `validate_request_overlay` 之后新增 `validate_caller_dimensions()`,按 Phase 组织(照搬既有风格): +- Phase 1 `tenant_id`: `None` 直接放行;非 `str` 报错;`strip()` 后为空报错(空串是哨兵值的地盘);长度 > 128 报错。 +- Phase 2 `meta` 键形态: 非 `str` 报错;不匹配 `^[a-z0-9_.]{1,64}$` 报错;以 `pg_` 开头报错(保留前缀)。 +- Phase 3 `meta` 键数量: > 16 报错。 +- Phase 4 `meta` 值: 类型不属 `(str, int, float, bool)` 报错(注意 `bool` 是 `int` 子类,先判 `bool` 无妨,两者都合法);`float` 且 `not math.isfinite(v)` 报错;`str` 且长度 > 256 报错。 +- 返回 `(tenant_id, dict(meta or {}))` —— 拷贝,防调用方复用同一 dict 逐次改值造成竞态(同 `overlay` 先例)。 + +`ChatRequest` 增两字段(见"关键接口")。字段 docstring 说明: 只读快照,库内中间件永不修改;`meta` 不进缓存 key(`cache_namespace` 已负责租户隔离,ARCH §7.5)。 + +**验收标准**: 每条红线抛 `ValueError` 且消息含 `origin`;合法输入返回浅拷贝且与入参不是同一对象。 + +**测试要求**(先失败后通过): +逐条红线各一个用例——`tenant_id` 空串/纯空白/超长/非 str;`meta` 键非 str/含大写/含连字符/超 64 字符/`pg_` 前缀/17 个键;值为 `list`/`dict`/`None`/`nan`/`inf`/`-inf`/超 256 字符的 str。另加合法路径用例: `tenant_id=None` + `meta={}` 放行、`meta` 值为 `bool`/`int`/`float` 有限值放行、返回值是拷贝(改返回值不影响入参)。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_types.py -v` → 全 PASS + +- [ ] 提交点: `feat: validate the dimensions a caller may attach to a call` + +--- + +## Task 2: 端口与两个遥测后端 schema + +**文件**: `src/polygateway/ports.py`、`src/polygateway/telemetry/sqlite.py`、`src/polygateway/telemetry/postgres.py`、`tests/unit/test_ports.py`(均修改) + +**行为**: + +`ports.py`: `record_llm_call` 增 `tenant_id: str` 与 `meta: str`(无默认值),docstring 的"22 字段冻结"改为 24 并说明新字段已归一化。 + +`sqlite.py` 三处同步改(顺序必须一致): +- `_DDL` 在 `reasoning_tokens` 之后追加 `tenant_id TEXT NOT NULL DEFAULT ''` 与 `meta TEXT NOT NULL DEFAULT '{}'`; +- `_BACKFILL_COLUMNS` 追加 `("tenant_id", "TEXT NOT NULL DEFAULT ''")` 与 `("meta", "TEXT NOT NULL DEFAULT '{}'")` —— SQLite 硬性要求 `NOT NULL` 列必须带非 NULL 常量默认值,缺默认值会报 `Cannot add a NOT NULL column with default value NULL`; +- `_COLUMNS` 追加两项。 + +`postgres.py` 同三处: +- `_DDL` 追加 `tenant_id TEXT NOT NULL DEFAULT ''` 与 `meta JSONB NOT NULL DEFAULT '{}'::jsonb`; +- `_BACKFILL` 追加两条 `ALTER TABLE llm_calls ADD COLUMN ...`(默认值均为非易失常量,PG 11+ 不重写全表); +- `_COLUMNS` 追加两项。 + +**新列必须排在末尾**(`created_at` 与既有补列之后)——旧表只能 ALTER 追加,新建库若插在前面两条路径的物理列序会分叉(`sqlite.py:53` 既有注释)。 + +**验收标准**: 两个后端的 `_COLUMNS` 逐字同名同序;新建库与旧表补列后列集合一致。 + +**测试要求**(先失败后通过): 扩展现有列序断言测试,断言两后端 `_COLUMNS` 相等且末两项为 `("tenant_id", "meta")`;`test_ports.py` 断言 `record_llm_call` 的参数集合含新两项且**无默认值**(用 `inspect.signature` 实测,不凭记忆)。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_ports.py tests/unit/test_telemetry.py -v` → PASS + +- [ ] 提交点: `feat: give the telemetry table a tenant column and a meta container` + +--- + +## Task 3: Emitter 透传与归一化 + +**文件**: `src/polygateway/middleware/telemetry.py`(修改)、`tests/unit/test_telemetry.py`(修改) + +**行为**: + +`_record` 增两个形参,并在传给 recorder 前归一化: `tenant_id or ''`;`json.dumps(dict(meta), sort_keys=True, ensure_ascii=False, allow_nan=False)`,空 dict 直接用字面量 `'{}'`。 + +`allow_nan=False` 是第二道闸(主防线是 Task 1 的入口校验)——`json.dumps` 默认把 `nan` 写成 `NaN` 字面量,那不是合法 JSON,PG 的 JSONB 会拒收,失败会被 emitter 的降级 try 吞成 warning,即把调用方的输入错误变成静默丢遥测。 + +三个 emit 入口统一从 `request.tenant_id` / `request.meta` 读取,不各自组装(遥测调用点收敛铁律): +- `emit_attempt`、`emit_terminal_failure`: 直接读 `request`; +- `emit_cache_hit`: **同样读 `request` 而非缓存中的历史响应**——维度是"本次调用由谁发起",不是历史那次。 + +**验收标准**: 三条路径写出的行都带维度;`tenant_id=None` 落 `''`;`meta={}` 落 `'{}'` 而非 NULL。 + +**测试要求**(先失败后通过): 用 fake recorder 断言三个入口各自收到的 `tenant_id`/`meta` 值;`emit_cache_hit` 单独一个用例——构造"请求带租户 A、缓存中的历史响应属于租户 B"的场景,断言落库的是 **A**(这是最容易实现反的一处);`meta` 序列化后键有序(`sort_keys=True`,便于跨行比对)。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_telemetry.py -v` → PASS + +- [ ] 提交点: `feat: carry caller dimensions through the single telemetry helper` + +--- + +## Task 4: `chat()` 公共入口 + +**文件**: `src/polygateway/client.py`(修改)、`tests/unit/test_client.py`(修改) + +**行为**: `chat()` 签名末尾增 `tenant_id: str | None = None` 与 `meta: Mapping[str, Any] | None = None`(带默认值的 keyword-only,签名冻结承诺不破)。在既有 `validate_request_overlay` 调用旁调 `validate_caller_dimensions(..., origin="chat(tenant_id=..., meta=...)")`,把返回值填进 `ChatRequest`。校验必须在**进洋葱之前**——洋葱内的一切失败都会被遥测层降级成 warning,校验放里面等于没有校验。 + +**验收标准**: 不传两参数时行为与改动前逐字一致(既有调用点零改动);传入非法值时 `chat()` 抛 `ValueError` 且**未产生任何遥测行**。 + +**测试要求**(先失败后通过): 端到端——`chat(..., tenant_id="t1", meta={"batch": "b-42"})` 后 fake recorder 收到的行带这两个值;非法 `meta` 抛 `ValueError` 且 recorder **零调用**(断言"校验早于遥测",这是 §4.2 的核心承诺);不传参数时 recorder 收到 `''` 与 `'{}'`。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_client.py -v` → PASS + +- [ ] 提交点: `feat: let chat() take a tenant and caller-defined dimensions` + +--- + +## Task 5: `embed()` 链路透传 + +**文件**: `src/polygateway/embedding.py`(修改)、`tests/unit/test_embedding.py`(修改) + +**行为**: `embed()` 增两个 keyword-only 参数并在入口校验(`origin="embed(tenant_id=..., meta=...)"`),沿 `_embed_batch()` → `_attempt()` → `_emit()` **逐层透传**,在 `_emit()`(`embedding.py:360`)构造 `ChatRequest` 时填入。 + +该链路已在逐层传 `session_id`/`parent_call_id`,再加两个即四个同类参数。**不顺手把它们收成值对象**——那会改动 embedding 全部内部签名,属任务外重构。本次只做加法。 + +**验收标准**: 多批(`texts` 长度 > `batch_size`)时**每一批的行都带同一份维度**——维度属于本次 `embed()` 调用,不随批次变化。 + +**测试要求**(先失败后通过): 单批与多批各一个用例,断言 fake recorder 收到的**每一行**都带维度(多批用例要断言行数 > 1 且全部一致,否则"只有第一批带维度"的实现会漏网);非法值抛 `ValueError` 且零遥测。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_embedding.py -v` → PASS + +- [ ] 提交点: `feat: carry caller dimensions down the embedding chain` + +--- + +## Task 6: OCR 链路透传(设计范围外,见文首说明) + +**文件**: `src/polygateway/ocr.py`(修改)、`tests/unit/test_ocr_client.py`(修改) + +**行为**: `recognize_text()` 与 `parse_layout()` 各增两个 keyword-only 参数并在入口校验(`origin` 分别标明方法名),沿 `_call()` → `_attempt()` → `_emit()` 透传,在 `_emit()`(`ocr.py:398`)构造 `ChatRequest` 时填入。结构与 Task 5 同构。 + +**验收标准**: 两个公共方法都覆盖(只改一个即漏)。 + +**测试要求**(先失败后通过): 两个方法各一个用例,断言遥测行带维度;非法值抛 `ValueError` 且零遥测。 + +**验证命令**: `conda run -n PolyGateway pytest tests/unit/test_ocr_client.py -v` → PASS + +- [ ] 提交点: `feat: carry caller dimensions through the OCR chain` + +--- + +## Task 7: 真实后端集成验收 + +**文件**: `tests/integration/test_postgres_telemetry.py`(修改)、`tests/unit/test_telemetry.py`(补 SQLite 真实文件用例) + +**行为**: 覆盖三件事,每件两个后端各测一遍。 + +1. **新建库**: 表列齐全,写入后读回维度一致。 +2. **旧表补列(不可逆性的机械化验收)**: 手工建一张 **22 列的旧表**并插入一行,再用当前 recorder 打开它 → 补列成功、新行写入成功、**老行的 `tenant_id` 读出为空串而非 NULL**。这条直接对应 issue 的核心论点(先启用后加列,老行归属无法还原);断言"是空串"而非"是 NULL",因为 NULL 在 RLS policy 下是对所有人永久不可见的黑洞。 +3. **补列失败的降级方向**: 权限不足时逐行降级丢弃而非判死(沿用 issue #9 既有测试形态,不新造机制)。 + +**验收标准**: PG 与 SQLite 行为对称;补列走既有 `_BACKFILL`,不新增 DDL 路径。 + +**测试要求**: 上述三项即测试本体。Postgres 用真实实例(CLAUDE.md §4.6: Redis/PG 相关测试不 mock)。 + +**验证命令**: +`conda run -n PolyGateway pytest tests/integration/test_postgres_telemetry.py -v` → PASS +`conda run -n PolyGateway pytest tests/ -q` → 全套件 PASS(**命令末尾不接管道**,否则退出码失真) + +- [ ] 提交点: `test: prove old telemetry tables gain the tenant column safely` + +--- + +## Task 8: 文档同步 + +**文件**: `README.md`、`CHANGELOG.md`、Gitea wiki(按 `research-wiki/docs-convention.md` §2 清单) + +**行为**: + +- **CHANGELOG**: 新增"未发布"段,写清新增两列、两个新参数(三条链路)、校验规则与上限数值、**以及库不建索引/不启用 RLS 的边界**。 +- **README**: 能力表补调用方维度;数字型断言若涉及遥测字段数,用 `inspect.signature` **实测**后再写(发布流程 §4.4.1 第 1 步的教训)。 +- **wiki**: 新增一节"多租户与自定义维度",含设计 §4.5 定稿的 RLS 模板(`ENABLE` + `FORCE` + `USING`/`WITH CHECK` 双写 + `NULLIF(current_setting(..., true), '')`)与复合索引 `(tenant_id, created_at)`,并写明三个陷阱: 表属主默认豁免 RLS;租户上下文必须在**显式事务内**用 `set_config(..., true)`(asyncpg 默认 autocommit,单发 `SET LOCAL` 会当场失效而 PG 只发 warning 不报错,表现为 fail-closed 到零行);只写 `USING` 不写 `WITH CHECK` 时租户 A 能插入标着 B 的行。 +- wiki 必须明确: **执行这些 DDL 是下游 DBA 的职责,库不会代劳**;不执行则 `tenant_id` 只是一个普通列,没有数据库层强制。 + +**验收标准**: 三处文档对"库做什么、下游做什么"的表述一致,不出现"库自动启用 RLS"之类的措辞。 + +**验证命令**: `conda run -n PolyGateway make lint` → PASS;人工核对 wiki 页面渲染。 + +- [ ] 提交点: `docs: document caller dimensions and the RLS template` + +--- + +## 全局验收 + +- [ ] `conda run -n PolyGateway make lint` → PASS(含 import-linter 依赖契约) +- [ ] `conda run -n PolyGateway make test` → PASS,覆盖率不低于改动前 +- [ ] 派全新上下文 verifier subagent 独立验证(CLAUDE.md §3 Phase 2 合并前硬门) +- [ ] 三条链路各自的"传入维度 → 落库"证据齐全(chat / embed / OCR) +- [ ] 旧表补列后老行读出空串的证据(issue 核心论点的验收) diff --git a/research-wiki/plans/issue11-caller-dimensions.md b/research-wiki/plans/issue11-caller-dimensions.md new file mode 100644 index 0000000..df32d6b --- /dev/null +++ b/research-wiki/plans/issue11-caller-dimensions.md @@ -0,0 +1,9 @@ +--- +type: plan +node_id: plan:issue11-caller-dimensions +title: "调用方自定义维度实现计划(issue #11)" +date: 2026-08-17 +--- + +# 调用方自定义维度实现计划(issue #11) +