保持 Agent Skill 文档与代码同步:PostHog 仪表盘组件维护文档架构指南
Dashboard widget(仪表盘组件)平台是 PostHog 中一个横跨后端注册表、OpenAPI 生成、前端 Zod 类型与 MCP 工具的复杂系统。当工程师修改 widget_specs/、registry.py 或任何组件相关代码时,如果配套的 Agent Skill 文档没有同步更新,就会出现「文档与代码漂移」——Agent 依据过期文档给出错误指引。本文基于 skill-maintenance.md 系统讲解 PostHog 如何通过「单一权威文档 + 触发路径 + 变更映射 + 最小清单」这套机制,让 /manage-dashboard-widgets 技能文档始终与仓库代码保持一致。读完你将掌握:何时需要触发文档维护、每个主题该去哪里更新、如何借助 CI 与 codegen 避免手工维护类型清单,以及新增/更新/废弃组件类型时各自的最小文档清单。
两条铁律:同一 PR 提交 + CI 强制对等
skill-maintenance 开篇即定义了文档维护的两条不可妥协的规则:
- 与代码变更放在同一个 PR 中(Same PR as the code change)——只要 agent-facing 行为或贡献者工作流发生变化,文档必须在同一个 PR 内更新,不允许「代码先合并、文档后补」。
- 注册表对等由 CI 强制,不在 Markdown 中手工维护类型清单(Registry parity is CI-enforced in code tests — do not hand-maintain type lists in markdown)——
EXPECTED_WIDGET_TYPES、catalog 键、registry.test.tsx这些「运行时真相」都活在代码与测试里,文档只负责解释,不负责复制清单。
这两条规则的深层动机可以从源码中找到印证:在 registry.py 中,DashboardWidgetType 是一个 Literal 联合类型,EXPECTED_WIDGET_TYPES 与 WIDGET_SPECS 的键由 test_run_widgets.py 断言必须完全一致;前端的 DASHBOARD_WIDGET_REGISTRY 则通过 satisfies Record<DashboardWidgetCatalogKey, DashboardWidgetDefinition> 在编译期约束 catalog 键与注册表一一对应。类型清单一旦被复制进 Markdown,就会成为一处必然过期的平行真相——这正是该文档禁止手工维护列表的根本原因。
文档架构:一个主题只有一个权威文档
为避免重复,skill 采用了「每主题一个 canonical doc,其他文档一律链接过去,不复制表格或长篇段落」的架构。以下是主题到权威文档的映射(全部路径已转换为仓库根目录相对路径):
| 主题 | 权威文档 |
|---|---|
| Intake / 规格问题 | widget-intake.md |
| 新增流程(文件与顺序) | checklist-new-widget-type.md |
| 模型、命名、扩展规则 | architecture.md |
| 配置契约 / codegen | config-and-codegen.md |
| Codegen 与 CI(本地 + 漂移) | config-and-codegen.md § Codegen & CI |
| 产品级 RBAC | permissions-and-sharing.md § Product RBAC |
| 瓦片最小/最大尺寸 | layout-and-ux.md § Tile min/max size |
| 注册表条目形态(代码) | architecture.md |
| 更新流程 | managing-existing-widgets.md 与 SKILL.md §3 |
| 验证命令 | SKILL.md §6 Verify |
| 人工入口 / 导航 | products/dashboards/CONTRIBUTING.md |
这套架构的精髓在于「去重」:同一份表格、同一段长文只存在于一个位置,其他文档通过相对链接引用。例如瓦片最小尺寸的完整指南只写在 layout-and-ux.md 的 § Tile min/max size 一节,其余文档(如 managing-existing-widgets.md、architecture.md)只链接过去,避免多个文档各自维护一份会互相打架的尺寸说明。
何时需要触发文档维护:触发路径清单
只要以下任一路径发生变化,就应加载 /manage-dashboard-widgets 技能并完成维护清单。这张表本质上是「代码变更 → 是否影响文档」的判定器:
| 触发路径(glob) | 示例变更 |
|---|---|
products/dashboards/backend/widgets/** | 新增 run_*、配置校验、查询接线 |
products/dashboards/backend/widget_specs/** | Pydantic 配置模型、校验、OpenAPI、注册表清单 |
products/dashboards/backend/widget_specs/pydantic_openapi.py | model_json_schema() 注入、DashboardWidgetConfig 的 oneOf(供 Orval 使用) |
products/dashboards/backend/widget_registry.py | 仅重导出——真正的编辑在 widget_specs/registry.py |
products/dashboards/backend/widget_catalog.py | 派生目录——label/availability 请编辑 registry.py 的 WidgetSpec |
products/dashboards/backend/api/widget_openapi_serializers.py | 重导出——OpenAPI 序列化器派生自 WIDGET_SPECS |
products/dashboards/backend/api/test/dashboard_openapi_test_helpers.py | dashboard PATCH OpenAPI 契约排除项(供 test_dashboard_openapi.py) |
bin/build-dashboard-widget-types.py | widget-date-from-options.json、widget-form-fields.json、源自 WIDGET_SPECS 的 ENUM 预检 |
tools/openapi-codegen/package.json(orval 版本) | 组件目录 Zod 要求 Orval 8.14+ 的 generateReusableSchemas |
tools/openapi-codegen/src/zod-postprocess.mjs | fixNullDefaults、annotatePureZodExports——共享的 Orval Zod 后处理 |
tools/openapi-codegen/src/schema.mjs | discoverComponentSchemaNames、discoverCatalogEntryConfigPropertyKeys |
products/dashboards/frontend/bin/generate-widget-config-zod.mjs | Orval generateReusableSchemas → widget-config-schemas/*.zod.ts + widget-configs.zod.ts |
products/dashboards/backend/api/test/test_widget_config_schema_parity.py | catalog config_schema ↔ Pydantic 对等性 |
products/dashboards/frontend/widgets/widgetConfigSchemaParity.test.ts | FE Zod 键 ↔ widget-config-property-keys.json |
posthog/openapi/enum_collisions.py | find_enum_collisions 的共享枚举冲突逻辑与 CI |
products/dashboards/backend/api/dashboard.py | run_widgets、批量添加、分享序列化器(仅通用逻辑) |
products/dashboards/backend/widget_query_throttle.py | run_widgets 的每团队突发/持续配额 |
products/dashboards/backend/widget_access.py | RBAC 拒绝文案 |
products/dashboards/frontend/widgets/** | 组件、编辑弹窗、注册表、预览 |
products/dashboards/frontend/widget_types/** | 目录、widgetConfigShared.ts UI 标签、availability |
products/dashboards/frontend/generated/widget-config-*.ts | 仅重新生成——不要手工编辑;codegen 输出变化时更新架构/清单文档 |
products/dashboards/frontend/generated/widget-config-schemas/** | Orval 可复用的每组件 Zod——通过 hogli build:widget-types 重新生成 |
posthog/settings/web.py(ENUM_NAME_OVERRIDES) | 新增按类型 widget_type 的 OpenAPI 枚举冲突覆盖 |
products/dashboards/frontend/components/WidgetCard/** | 共享瓦片 chrome、占位、概览 fixtures |
products/dashboards/frontend/components/DashboardWidgetItem/** | 瓦片胶水、公共展示、TileFilters 挂载 |
products/dashboards/frontend/widgets/constants.ts | 列表 footer、抓取错误、瓦片刷新防抖毫秒数 |
frontend/src/scenes/dashboard/widgetTileRefreshScheduler.ts | 瓦片筛选 PATCH 后的防抖 run_widgets |
frontend/src/scenes/dashboard/dashboardLogic.tsx | scheduleRefreshDashboardWidgets 与立即刷新 |
products/dashboards/frontend/widgets/*WidgetTileFilters.tsx | 瓦片内筛选器(日期、状态、属性选择器) |
frontend/src/scenes/dashboard/DashboardItems.tsx | showEditingControls、isDashboardEditMode、瓦片筛选挂载 |
products/dashboards/mcp/tools.yaml | 组件 MCP 工具 |
frontend/src/scenes/dashboard/tileLayouts.ts | 布局算法(仅当行为/文档变化时) |
posthog/api/test/test_sharing.py | 共享 dashboard 组件 payload 期望 |
tach.toml(products.dashboards 的 depends_on) | 新增产品导入边界 |
需要特别说明的是:纯平台重构(platform-only refactors)只要不改变行为、不改变 agent-facing 表面,可以跳过叙事性文档更新,但仍然必须运行 Verify 测试。
变更 → 文档映射表:改了哪里就去更新哪份文档
与上面的「触发路径」互补,「变更 → 文档映射」从工程师的实际改动出发,直接告诉你该去更新哪份 skill 文档:
| 你改了什么 | 在技能中更新哪里 |
|---|---|
新增或删除 widget_type | 新增流程变化时更新 checklist-new-widget-type.md;ENUM_NAME_OVERRIDES 见 config-and-codegen.md § Codegen & CI;发现新的不变量则补 footguns |
| 配置字段 / 校验 | config-and-codegen.md;managing-existing-widgets.md § Config schema migration;对等测试见 SKILL.md §6 Verify |
| 公开/共享/导出行为 | permissions-and-sharing.md;SKILL.md §4 不变量 |
| 安装/availability 门槛 | availability-and-gating.md;BE 的 availability_requirements 注意事项 |
| 瓦片布局 / 最小尺寸 / 添加位置 | layout-and-ux.md;REST/MCP 添加路径变化时更新 architecture.md |
| WidgetCard / 编辑弹窗组合 | composition.md |
瓦片筛选栏 / widgetFilters 配置 | list-widget-patterns.md |
列表分页 footer / run_* 总量 | list-widget-patterns.md(dashboard 路径上的 include_total_count) |
run_widgets 限流 | composition.md 或 architecture.md;widget_query_throttle.py + 产品列表限流(replay) |
| 筛选 PATCH 后的防抖瓦片刷新 | constants.ts 的 WIDGET_TILE_REFRESH_DEBOUNCE_MS;dashboardLogic.tsx |
| 头部标题链接 / dashboard 编辑模式 | list-widget-patterns.md;layout-and-ux.md 的 ⋯ 菜单对等性 |
| MCP 工具或 Agent 流程 | mcp.md |
| 新产品域 / tach / UI 复用模式 | checklist-new-widget-type.md §4c |
| 人工贡献者入口 | products/dashboards/CONTRIBUTING.md 的注册表表格 / Verify 块 |
维护原则是:优先编辑现有 reference,而不是新增文件。SKILL.md 始终扮演索引的角色,细节一律下沉到 references/ 目录中。
三份最小维护清单
新增 widget_type(Add checklist)
完整代码清单见 checklist-new-widget-type.md,此处是文档侧的最小更新项:
- checklist-new-widget-type.md —— 若新增流程或易遗漏路径有变化则更新
- architecture.md —— 若发现了值得记录的新 footgun 或平台不变量
-
CONTRIBUTING.md—— 若 Verify 命令或注册表对等表格有变化
更新已有类型(Update checklist)
- managing-existing-widgets.md —— 若变更具备可复用性,扩展路由表或迁移说明
-
mcp.md—— 若 Agent 工作流或工具语义发生变化
移除 / 废弃一个类型
- 只有当没有瓦片引用该类型时,才先从注册表移除(或记录 unknown-type 回退机制)
- 若废弃流程有变化,在 managing-existing-widgets.md 的 § Deprecating 下补充说明
值得注意的是,「废弃」没有软删除机制:widget_type 字符串是不可变的,已存在的 DashboardWidget 行无法就地修改类型;同时不要为仍存在 posthog_dashboardwidget 行的类型移除后端校验,孤儿瓦片会回退到 unknown-type UI(header 回退 + body ErrorBoundary)。
文档与代码一起验证:Verify
验证命令的 canonical 清单只存在于 SKILL.md §6 Verify,其他文档一律链接过去,不在别处复制命令列表。核心验证分三层:
MVP smoke(渲染 + 核心测试,非发版点):
hogli test products/dashboards/backend/api/test/test_run_widgets.py
hogli test products/dashboards/backend/api/test/test_dashboard_widgets.py
hogli test products/dashboards/frontend/widgets/registry.test.tsx
发版门槛(PR 前必须完成): checklist §8 + hogli build:openapi + 每个新类型必须配套专用 Storybook stories(组件 + 编辑弹窗,二者缺一不可,仅 catalog 概览故事不算数)。
Config SSOT 变更(编辑 widget_specs/ 后同样要跑):
hogli test products/dashboards/backend/api/test/test_widget_config_schema_parity.py
hogli test products/dashboards/frontend/widgets/widgetConfigSchemaParity.test.ts
hogli test products/dashboards/backend/api/test/test_widget_openapi_enums.py # 仅新增 widget_type 时
更新路径上,至少覆盖所触碰层的测试:后端改动跑 test_run_widgets.py,前端改动跑 products/dashboards/frontend/widgets/ 下的测试,config OpenAPI 变化则跑 hogli build:openapi。
什么不该在文档中重复
文档维护的边界同样重要——以下两类内容严禁复制进 Markdown:
- **运行时真相(Runtime truth)**留在代码与测试中:
EXPECTED_WIDGET_TYPES、catalog 键、registry.test.tsx。不要在文档里维护一份平行的类型列表。 - 生成文件(Generated files)(
frontend/generated/*、MCP schema JSON):一律用hogli build:openapi重新生成,不要逐字段复制成文档。
从源码看,这份「不要重复」的底气来自 registry.py 的自洽设计:
WIDGET_SPECS是唯一的WidgetSpec清单(registry.py),每个条目携带config_model、query_fn、required_scopes、group_id、label、description、required_product_access、availability_requirements、form_fields、filter_fields等元数据——catalog、OpenAPI、前端 Zod 全部由此派生;validate_widget_config()通过config_model.model_validate(config)做 Pydantic 校验,再以model_dump(mode="json", exclude_none=True)输出规范化的配置字典,类型校验完全收敛在代码层;WidgetSpec.__post_init__甚至会在构造时校验 live widget 的非法配置字段(dateRange、filterTestAccounts被_LIVE_FORBIDDEN_CONFIG_FIELDS禁止),把「文档要写」的不变量直接前置到代码运行时。
也就是说,文档只负责解释「去哪里改、改了会影响什么」,而「什么值合法、什么组合不允许」全部由代码与测试兜底——这正是 CI 强制对等原则能够成立的基础。
落地实践:一次真实维护的完整路径
结合 products/dashboards/CONTRIBUTING.md 的 Frontend/backend parity 表格,一次完整的文档维护应当这样落地:
- 改代码:编辑
widget_specs/configs.py(Pydantic SSOT,先改这里)→registry.py(WidgetSpec条目)→run_*runner。 - 跑 codegen:
hogli build:openapi一条命令完成openapi-schema → build:widget-types → openapi-types → MCP全链路,重新生成products/dashboards/frontend/generated/*(Zod、property-keys、date-from-options、form-fields),并提交生成差异。 - 过 CI:
ci-backend.yml中的check-openapi-types会重跑同一命令并对 diff——本地未提交生成文件时 PR 会失败并提示「runhogli build:openapilocally」;同仓库 PR 可能被自动提交漂移。 - 更新文档:按「变更 → 文档映射」表找到对应 reference,在同一 PR 内完成最小清单,然后运行 Verify 命令。
- 自查对等:参考 CONTRIBUTING.md 的 parity 表格逐层核对——Pydantic 模型、注册表清单、catalog、OpenAPI、FE Zod、property keys、date presets、form fields、UI catalog、previews、运行时注册表是否全部对齐。
这套「代码变更 → 触发路径判定 → 权威文档映射 → 最小清单 → Verify 兜底」的闭环,保证了 /manage-dashboard-widgets 这个 Agent 技能始终与仓库的真实行为一致——文档因代码而生,也随代码而变,唯一不变的是 CI 与 codegen 构成的强制对等防线。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



