fix: bug 批次修复(PM 派发循环)— #606 #673 #610 #548 #678 #650 已落地 - #677
Conversation
`sales_copilot` 在 #512 已退役(应用自撰 agent 全部移除,表面改为 skills-only, 见 ADR-0063 §2),#586 清掉了 src/ 下的两处引用,但旗舰 "Wow #1 — live schema" 示范仍在四处把它当作 `POST /api/v1/ai/chat` 的 `agent`:可执行脚本 + 三个语系文档。 这是一条会真正跑挂的路径——`loadAgent()` 拒绝非平台 agent 名,脚本跑在 `curl -fsS` 下,第 2 步直接中断;文档则把同一个 body 当作可复制粘贴的 curl 发出去。 四处统一改为平台 agent `ask`(显式写出而非省略,理由见下),周边行文同步改成真实 架构:HotCRM 的 `live_data` skill 挂载在 `ask` 上,而不是一个应用自撰的 copilot。 三个语系保持同步。objectstack.config.ts 的注释里 "the two agents + skills still validate" 一并改正——#512 之后这里已没有任何 agent(同时完成 #612 第 2 项)。 test/docs-drift.test.ts 新增守卫:把文档代码块与示范脚本里出现的每个 `agent:` / `defaultAgent:` 取值,钉在从 spec 读出的平台 agent 集合(`AgentSchema.shape.surface`) 上——与 metadata-references.test.ts 对 `App.defaultAgent` 用的是同一处推导,两者覆盖 同一契约的两个表面。这也是这里显式写 `"ask"` 而非按 ADR-0063 §1 省略该键的原因: 省略会让守卫无输入可查,沦为空转。守卫自带 vacuity 断言,扫不到任何样本即报红。 反向验证(方向为事先预判,两个方向都已跑过): - 把四处任一改回 `sales_copilot` → 新守卫转红,并逐文件点名(已实测脚本 + 英文档两条)。 - 把四处 `agent` 键全部删除 → 不是静默转绿,而是 vacuity 断言报红。 Refs #606
… the raw id
The activity bodies stamped `ctx.user?.name`, which is NOT a display name on
the dispatch path the Console uses: @objectstack/runtime 17.0.0-rc.2 builds the
REST action context's user as `{ id: ec.userId, name: ec.userId, ... }`
(dist/index.js:5397), so the key is present and carries the id. Every timeline
row written from a record page therefore showed a 32-character id where the
actor's name belongs. The MCP path (dist/index.js:1776) prefers
`ec.userName ?? ec.userDisplayName`, but nothing populates either field, so it
lands on the id as well.
The shared activity body now resolves the name from `sys_user.name` and keeps
the id only as a last resort — an unattributable activity is worse than an ugly
one. A user object already carrying a name different from its id is believed
as-is, so this is one query on the broken path and none on a working one, and
the block deletes itself the day the platform honours `ctx.user.name`.
Tests run the REAL dispatcher user shape (`name === id`), which is why the
pre-existing "carries the acting user" guard stayed green through the whole
bug: the harness default (`{ id: 'usr_1', name: 'Ada Lovelace' }`) is a shape
REST never produces.
Refs #673
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
…uard the list Refs #610 content/docs/analytics/dashboards.mdx listed tiles that no dashboard declares — CRM Overview 与 Executive 与真实 metadata 的重合度为零 — 并声称 "Cases Approaching SLA 是本仪表盘点击最多的小部件":该磁贴不存在, 且仓库中没有任何点击遥测,两个前提都是编造的。该说法直接删除,不以另一个 未经测量的最高级替换。 页面改为按当前注册的五个仪表盘逐条描述真实 widget title,其中 sales_activity_dashboard(#592 / PR #670 加入)此前完全没有文档。同时纠正 三处实质性错误:#587 之后任何磁贴都不显示同比趋势;Sales Activity 与 Customer Service 刻意没有日期范围选择器(#460 的 datetime 过滤缺陷); 导出 PDF、周报订阅、阈值告警等本应用并未实现的能力清单被替换为各仪表盘 真正声明的控件。 test/docs-drift.test.ts 新增 dashboards 规则(不改动 #606 的既有规则): 章节内的每个磁贴条目必须解析到该仪表盘的 widget title,正文中的 `**Name** tile` 引用必须解析到某个仪表盘的 widget,且每个已注册的仪表盘 都必须在页面上有章节 —— 最后一条正是第五个仪表盘无文档发布时会触发的。 三处 vacuity 断言保证该规则不会在零输入上变绿。
…d` (Refs #548) Every business object authored its own `owner` lookup while the platform kept a separate `owner_id` beside it. Only `owner_id` was ever read by OWD, sharing rules, owner-scope widening or the #547 `is_private` row filter — so "Owner" on a form was a label, not a permission: reassigning it moved the record in every list, view and report and moved no access at all, records created on someone's behalf stayed scoped to their creator, and any org design built on the visible field silently did something else. Option B per the recorded decision (2026-08-02): drop the app-authored field and surface the platform column. - `owner` removed from all 12 objects that carried it (account, campaign, case, contact, contract, event, forecast, knowledge_article, lead, opportunity, quote, task — `crm_event` included; it was added by #592 after the issue was filed). `renewal_owner` / other business-role lookups stay: they are not ownership. - `owner_id` is DECLARED on each of them rather than left to registry injection. Injection-only does not work for the surfaces the decision names: `os validate` reports `highlightFields: ['owner_id']` as "not a field on this object — silently skipped by every consumer" and rejects a CEL predicate reading `record.owner_id` outright, because author-time rules resolve fields against the authored schema. The platform documents author-declared fields as winning over injection; `system: true` keeps the marker its own tooling reads (the clone path strips system columns, so a copy is stamped to the cloner instead of inheriting — and being denied for — the source's owner). Per-object labels, field groups and `trackHistory` survive with it. - No `defaultValue: cel os.user.id`. The security middleware stamps the acting user onto any insert that leaves `owner_id` empty, which is stronger than a field default that evaluated to nothing on every user-less write (#620). Those boot warnings go with the fields. - Views, pages, highlight strips, dataset columns, dashboard global filters and drill-down columns, RLS predicates, translations (4 locales), import mappings, hooks, flows and seeds all repointed. Dataset DIMENSION names stay `owner` — they are semantic-layer aliases over the `owner_id` column, and the reports and widgets that select by name are unchanged. - `demo_bootstrap` collapses from a two-column sweep to one. The half-claimed state of #622 is now unrepresentable rather than guarded against. - `allowTransfer` granted deliberately: system_admin (every object), sales_manager (exactly the objects it holds modifyAllRecords on), service_agent (crm_task only — escalation opens the follow-up task under the account owner through `ctx.api`, which the guard does see). Verification item from the decision, measured rather than assumed (`test/ownership-model.test.ts`, real ObjectQL): the #3004 guard is an operation middleware, so which of our writers it can see is not uniform. 1. a `beforeInsert` hook mutating `input` is INVISIBLE to it — the hook phase runs inside the operation the middleware already wrapped. `lead_auto_assign` round-robins onto another rep with no transfer grant, and needs none. 2. a hook's `ctx.api` write is a NEW operation carrying the same context, so the guard does see it — this is what the service_agent grant is for. 3. `runAs: 'system'` flows and seeds short-circuit the middleware entirely: no gate, and no auto-stamp either, which is why demo_bootstrap still exists. Migration: `scripts/backfill-owner-id.ts` (`pnpm backfill:owner`, report-only without `--apply`) copies the displayed owner onto the enforced one where they diverge. It talks only to the REST API and MUST run before the upgrade — after it, the old column is unreadable. Co-Authored-By: Claude <noreply@anthropic.com>
…or_name `send_email` stamped `actor_name: ctx.user?.name ?? null` — the byte-identical defect fixed for the activity family in #673, on the same column. On the dispatch path the Console uses, `ctx.user.name` is not a display name: @objectstack/runtime 17.0.0-rc.2 builds the REST action context's user as `{ id: ec.userId, name: ec.userId, … }` (dist/index.js:5397), so the key is present and carries the id. Every logged email on a contact's timeline rendered a 32-character id where the sender's name belongs. The resolution block is now shared rather than copied: `global.actions.ts` exports it as body SOURCE TEXT (`ACTOR_NAME_RESOLUTION_SOURCE`) and both bodies splice it at authoring time. A helper function could not be shared — an action body ships body-only and runs in QuickJS with no module scope, so a call to an import is a ReferenceError at runtime; splicing a string while Node builds the metadata leaves each body self-contained. The generated activity bodies are byte-identical to before apart from two comment lines. Tests execute `send_email` under the REAL QuickJS harness with the dispatcher's own user shape (`name === id`) — the shape that kept the pre-existing `send_email` guard green through the whole bug, since the harness default (`{ id: 'usr_1', name: 'Ada Lovelace' }`) is one REST never produces — and pin that the value is NOT the raw id, that `sys_user` is read once and only when the dispatcher delivered no name, that a missing or throwing read falls back to the id instead of blanking the actor, and that no `actor_name` writer stops splicing the shared block. Refs #678
引擎只在三处求值流程条件:start 节点的 config.condition、decision 节点的 复数 config.conditions[]、以及每条出边的 condition。decision 节点上的单数 config.condition 不被任何 reader 读取:spec 的 DecisionConfigSchema 只声明 conditions 一个键,而 decision 不发布 descriptor configSchema,因此引擎针对 未声明配置键的拒绝检查(#4277)整类豁免它——任何一层都不会报错。 本仓库 8 个 decision 节点(campaign_enrollment、contract_renewal、 forecast_snapshot、opportunity_stagnation)如此声明。行为一直正确只因谓词被 复制到出边,且无任何机制强制同步:单独修改节点副本,走向不变且无声。 选择出边权威(2b)而非节点权威(2a),依据实测:8 个站点中 7 个是单出边 "跳过"闸门,假分支无处可去。节点声明 conditions[] 并去掉出边谓词后,条件为 假时执行器返回 branchLabel 'default',traverseNext 找不到认领该 label 或 isDefault 的出边,按设计回退为求值全部出边,而仅存的出边已无条件——闸门被 静默反转。要使其安全需为每个闸门臆造空节点作 isDefault 汇点。 新增 test/flow-decision-authority.test.ts:静态扫描(含 loop 体嵌套节点)+ 真实引擎上的两个可执行证明(惰性证明、失效为放行的实测)。 flow-variable-conditions.test.ts 的 "guard the guard" 断言原先要求至少存在 一个节点级 config.condition 站点——该断言只在缺陷存在时成立,且与该文件自身 注释矛盾,改为断言其为空。 Refs #650
…achable by (Refs #549) #549's recorded decision converts crm_quote and crm_contract to controlled_by_parent under crm_account so a territory-shared account carries its quotes and contracts. Before changing an OWD, measure what the engine does with one: this boots the shipped enforcement stack (ObjectQL + plugin-security + plugin-sharing over the app's own metadata), materialises the territory rule and asks, as a real sales_rep, what comes back. It does not follow the parent's sharing. A rep who can read exactly one account reads BOTH accounts' contacts, and both quotes' line items while able to read neither quote: computeControlledByParentFilter resolves the master id set through the master's RLS policies (this app authors none on crm_account) under a system context, so ownership and sys_record_share grants are never folded in. With the proposed conversion applied the same rep saw every quote and every contract in the org, and could edit a quote on an account they cannot read. So no OWD changes here. The measurement is pinned as a test, the coverage ledger's 'derived' entry now says what derived really means, and the shipped over-reach on contacts and line items is filed as #694.
…erage on #548's owner_id keys `main` advanced by 6809624 (#679), which completed en / es-ES / ja-JP on every translatable surface and retired the PENDING_SELECT_LABELS exemption ledger. The branch carries #548, which removed the app-authored `owner` lookup from all 12 objects that had it and declares the platform's `owner_id` instead — including in all four locale packs. Both sides edited the same field-map lines, so en.ts, es-ES.ts and ja-JP.ts conflicted (zh-CN.ts did not: #679 never touched it). Resolution keeps both, and neither side's edit is a superset of the other: - FROM main, the whole of #679's content: the 133 (en) / 355 (es-ES, ja-JP) added keys, the `pages` group that was absent from three bundles, the shared `leadSourceOptions` / `relatedToTypeOptions` spreads in ja-JP, and the label corrections the sweep found — notably `crm_account.owner` in ja-JP, which read 取引先責任者 (the object label for `crm_contact`) and is now 取引先所有者, and `crm_contact.owner`, now 取引先責任者の所有者. The branch had carried the old strings through its rename, so taking the branch's line would have silently reverted two of the drift fixes #679 shipped. - FROM the branch, the field KEY only: the 12 `owner:` entries per locale become `owner_id:`. An entry still keyed `owner` names a field no object declares any more — dead metadata that reads as coverage while the screen stays English, and exactly what `metadata-references.test.ts` "translated object and field keys name real objects and fields" exists to reject. Applied as `^\s+owner: {` only, so the business-role lookups that are NOT ownership keep their own keys: `crm_account.renewal_owner`, `crm_product.product_manager`, the zh-CN `ownership` field-group key, and the `pipeline_by_owner` / `open_pipeline_by_owner` / `win_rate_by_owner` report keys are all untouched. A blanket rename would have produced `renewal_owner_id`. test/metadata-references.test.ts took main's rewrite unchanged — the branch never edited it — and needed no adjustment: its locale guards derive field names from the loaded `objectstack.config` rather than hard-coding any, so they now assert the merged invariant (`owner_id` translated in all four locales) on their own. No assertion was weakened. Measured, not assumed — flattened leaf keys per pack, origin/main vs merged: en 1156 → 1156, es-ES 1216 → 1216, ja-JP 1216 → 1216, zh-CN 1280 → 1280, with the key-set delta being exactly the 12 `fields.owner.label` → `fields.owner_id.label` renames in each and nothing else. `pnpm verify` clean: validate ✓, tsc --noEmit ✓, lint 13 warning(s) (the same 13 #679 left, none i18n), hygiene ✓, build ✓, 55 test files / 1317 tests passed.
🔀 交接说明(给接手本批次的人 / 另一个 Claude 账户的会话)本批次由 PM 派发循环完成,会话 一、这个 PR 的处置
合并时会由正文的
二、本仓在本批次采用的分支约定(与默认不同,务必遵守)
三、
|
| Issue | 状态 | 备注 |
|---|---|---|
| #549 | needs-user-decision |
等裁定,不可派发 |
| #620 | P2,未认领 | 前提已被实测推翻;阻塞于本 PR 合并后复核启动告警归零即可关闭 |
| #694 | upstream:objectstack |
修法在平台(#5386);本仓侧的三处文档纠偏建议等 #549 裁定后一并做 |
| #685 | pm:queue,未认领 |
中文 dashboards 页漂移。本批次因维护者限定「排除国际化」而未派发,Blocked-by #610(需本 PR 合并后再译) |
| #686 / #695 | finding |
待发现分诊轮 |
| 上游跟踪组 13 条 | 保持 open | #664 #656 #567 #528 #526 #525 #524 #522 #521 #520 #510 #509 #508 —— 本仓侧无可修项 |
本批次新提的上游单:objectstack #5372、#5378、#5383、#5386(均已查重,无重复)。
六、跨账户协作提示
上表中被 assign 的 issue 都是已完成待合并,不是在飞 —— 接手者按「assignee 不是自己 ⇒ 不要碰」处理即可,不会漏活。真正可接的活是第五节里未认领的那几条。
main 移动很快(本批次期间 main 就前进了一次并造成语言包冲突)。基于本分支开新 worktree 前先 git fetch,并注意:src/translations/*.ts 是本仓的冲突热点,解决时要整体采用 main 的译文再重新施加键改动 —— 直觉上的「保留我方」会静默回退掉 main 刚修的翻译(本次就险些发生)。
Generated by Claude Code
Description
PM 派发循环的 bug 批次修复分支(维护者指定的统一开发分支)。每个 issue 由独立 os-dev agent 在专属 worktree 实现,经文件面互斥调度后串行合入本分支。每条均经 PM 对照远端与 CI 复核,非采信 agent 自述。
已落地:
sales_copilotagent #606 — Wow Implement metadata-driven enterprise CRM on @objectstack/spec protocol #1 demo 脚本与三语言文档不再 POST 已退役的sales_copilot,改用平台 agentask;objectstack.config.ts注释纠偏(同时完成 Docs drift (round 2 leftovers): ~39 product pages still name "Sales/Service Copilot" as a persona; config comment and RELEASE_STRATEGY.md still stale #612 第 2 项);docs-drift 守卫钉住「文档/脚本中的agent:值 ∈ spec 平台 agent 集」,含防空转断言。sys_activity.actor_name不再写裸 user id。平台真因(REST 派发路径硬编码name: ec.userId,三条派发路径 user 形状互不一致)已上报 objectstack#5372。dashboards.mdx按五个已注册 dashboard 的真实 widget title 重写(旧文档与 issue 均只说四个);删除编造的 most-clicked 统计;docs-drift 新增 listed⇒exists 规则。衍生中文页漂移立单 dashboards.zh-Hans.mdx / .zh-Hant.mdx still carry the drifted tile lists and the fabricated most-clicked claim fixed in #610 #685。ownerlookups vs the platform'sowner_id— reassigning Owner in the UI does not move record access #548(P0) — ownership 模型统一到平台owner_id:12 个对象(比 issue 所列多两个,含 Activity model: first-class meetings/events + call logging on every sales object #592 新增的crm_event)移除 app 层ownerlookup,全部表面改指;allowTransfer按角色授权并钉住;一次性回填脚本pnpm backfill:owner(默认只读)。业务角色字段renewal_owner/product_manager保留。send_email的同型缺陷。解析逻辑不是复制而是共享(ACTOR_NAME_RESOLUTION_SOURCE作为 action body 源文本导出),并有守卫断言每个写入方拼接同一个块。decisionnode's singularconfig.conditionis never evaluated — 24 flows author inert metadata #650 — decision 节点的惰性config.condition全部移除,出边成为唯一分支站点(方案 2b,经实测选出:方案 2a 在真实引擎上会让闸门静默反转)。根因(lint 规则族从不下降进 loop 体)上报 objectstack#5383。test/parent-derived-reach.test.ts);OWD 变更未落地,已升级维护者拍板,见下。Type of Change
Related Issues
Fixes #606
Fixes #673
Fixes #610
Fixes #548
Fixes #678
Fixes #650
Related to #549(待裁定)、#612、#620、#685、#686、#694、#695、objectstack-ai/objectstack#5372、#5378、#5383、#5386
Changes Made
ownerlookup,显式声明owner_id(带system: true);views / pages / highlightFields / 索引 / 数据集 / 仪表盘过滤与钻取 / RLS 谓词 / 4 个语言包 / 导入映射 / hooks / flows 同步改指src/profiles/:allowTransfer授权轴 + 规范说明scripts/backfill-owner-id.ts:一次性回填(需--apply才写)src/actions/:显示名解析块共享化,两个写入方拼接同一份src/flows/:8 处惰性 decision 条件移除,承重注释迁到真正生效的出边上content/docs/analytics/dashboards.mdx、content/docs/ai-copilot/live-schema*.mdx、scripts/wow1-live-schema.shtest/ownership-model.test.ts、test/flow-decision-authority.test.ts、test/parent-derived-reach.test.ts,以及 docs-drift / action-sandbox / global-actions 中的多条Testing
npm test) — 55 文件 / 1319 通过,1 skippednpm run lint) — 0 errors,warnings 均为 main 既有npm run build) — ✓Screenshots
N/A
Checklist
pnpm changeset) — required on every PR, or theskip-changesetlabel is appliedcontrolled_by_parent的派生不跟随主记录的共享,应用该决议会让报价/合同全组织可读、报价全组织可写。已置needs-user-decision,OWD 未做任何变更。ownerlookups vs the platform'sowner_id— reassigning Owner in the UI does not move record access #548 中两处 PM 决定,给否决窗口:(a)owner_id改为显式声明而非依赖注入(平台作者期校验器不认注入列,已上报 objectstack#5378);(b)service_agent获得crm_task的allowTransfer,超出书面决议的「等 escalation-reassignment 落地」—— 不给则客服升级工单整体 403;回退是一行,pin 测试已标明位置。Additional Notes
同批次分诊产出(不在本 PR 内):关闭前提过期单 #523、#575;#620 前提经实测推翻(真实用户路径正常求值,仅 seed 路径告警)已降级 P2;上游阻塞组 13 条保持跟踪。
🤖 Generated with Claude Code
https://claude.ai/code/session_0194QUW67v7vyLToEd9NSeRM