feat(presentation): compile a provider-neutral confirmation frame for a plan card - #4579
Conversation
… a plan card
LoopX already ships the Lark half of a card confirmation: a non-forwardable Card
2.0 with confirm/reject buttons for a typed `operation.execute` proposal, a
`card.action.trigger` consumer, operator membership and tenant verification,
replay protection, card readback and result-card patching. Reusing that shell for
a steward team plan was blocked on one thing: the card content is compiled from
`operation_review_frame_v0`, which can only describe an operation proposal because
its identity is the operation envelope.
`compileReviewCardFrame` now compiles `review_card_frame_v0` for a validated
`team.plan` proposal. Its identity is the proposal plus the state fingerprint the
apply re-validates against, its decisions are confirm/reject, and its body is the
data the plan already carries -- goal, objective, each lane with its Agent and
first bounded Todo or its gap and reason, the gap summary, the quota envelope and
the stop condition. Fixed labels stay keys (`titleKey`, `warningKey`, ...) and
fields are `{key, value}` pairs, so this boundary stays language-neutral and each
surface owns its own words. The frame is additive on `ActionReviewPlan`; nothing
posts a card yet and no surface changes what it renders.
The Lark README and the team-intake RFC record the reuse boundary this rests on,
including what a plan card still needs: a delivery route for the audience that
asked (a manager group is not a Goal channel binding), a callback effect that
applies through the Chat action service instead of claiming an operation
envelope, and an explicit answer for whether an external manager audience may
perform this durable write.
Validation: `tests/control_plane_ts/action_review_plan.test.ts` covers the frame
(identity, decisions, ready and gap lanes, envelope and stop condition, key-shaped
labels) and its refusals (not an admitted preview, another action kind, missing
state fingerprint, operation proposals keep the operation frame). docs governance
and `loopx canary premerge --from-git-diff` pass.
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
…frame The chat surface compiles its action review plan from the control plane, so the bundle carries `compileReviewCardFrame` now that it exists. Rebuilt with npm run build:chat: new entry assets/index-Cp9Ll6py.js, the previous generation assets/index-CXvjZarX.js stays retained and assets/index-Cb7S1yHW.js retires. No rendered surface changes in this commit. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
huangruiteng
left a comment
There was a problem hiding this comment.
Head reviewed: a5028fa
动机
核对"飞书卡片能力能否复用":Lark 半边(Card 2.0 投递、按钮回调、操作者校验、重放保护、读回、结果修补)已存在,但卡片内容来自只对 operation.execute 生效的 operation_review_frame_v0,团队计划因此没有可复用的卡片内容层。
改动思路
新增 language-neutral 的 review_card_frame_v0:身份 = proposal + state fingerprint,决策 = confirm/reject,正文 = 计划的既有数据,固定标签保持 key。加性挂在 ActionReviewPlan 上,复用同一 handler id。
具体改动
action_review_plan.ts 新增类型与 compileReviewCardFrame;TS 用例覆盖身份/决策/ready+gap lane/quota/stop/key 形状/四类拒绝;Lark README 与 RFC EN/ZH 记录复用边界与仍缺的三件事;第二笔 commit 出货 bundle。
对主干的风险
纯加性、不投递卡片、不改变既有 operation 路径与 apply 语义;帧只带数据与 key,不带授权。
我的整体评价
审查了自己的 diff:compileReviewCardFrame 对非准入预览/其他 kind/缺 fingerprint 一律 undefined,operation 提案不会被误判为计划卡片;测试断言 key 形状以守住"标签不下沉到控制面"这条边界。
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
English verdict: APPROVE
动机
业主问:LoopX 已经有飞书卡片能力,团队计划能不能复用? 我核了代码,答案是可以复用——但缺一块:
loopx/extensions/lark/goal_channel_operation.py已经交付了卡片确认的整个 Lark 半边(不可转发的 Card 2.0、确认/拒绝按钮、card.action.trigger消费者、操作者成员与租户校验、重放保护、卡片读回与结果卡片修补),并且卡片内容来自 language-neutral 的operation_review_frame_v0。问题在于:compileOperationReviewFrame只对operation.execute生效,因为它的身份是 operation envelope;团队计划没有 envelope,于是卡片内容这一层无法复用(内容目前只存在于 dashboard 的teamPlanFieldsreducer 里,是 app-local 的)。改动思路
compileReviewCardFrame为已校验的team.plan产出review_card_frame_v0。expected_state_fingerprint(不是 envelope),决策 = confirm/reject。titleKey/warningKey/confirmLabelKey...),字段是{key, value}对——控制面 TS 里一个中文都没有,语言属于渲染它的那个面。reviewCardFrame加性挂在既有ActionReviewPlan上,复用同一个 runtime handler id,Python 侧不需要新接口。具体改动
loopx/control_plane/presentation/action_review_plan.ts:新增ReviewCardFrame类型与compileReviewCardFrame(未准入的预览、其他 action kind、缺 fingerprint 一律返回undefined;operation 提案仍走 operation frame)。tests/control_plane_ts/action_review_plan.test.ts:覆盖身份/决策/ready 与 gap lane/quota/stop、key 形状(^[a-z][a-z0-9_]*$)、以及四类拒绝。loopx/extensions/lark/README.md:在 "Human-confirmed typed operations" 一节写清复用边界。对主干的风险
operationFrame路径、卡片渲染、回调、apply 语义都不变;目前不会投递任何计划卡片。field_coverage只覆盖 preview 形态;卡片里 quota/stop 是展示数据,不是执行约束(RFC 里已有的既有限制,未改变)。我的整体评价
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
这一刀刻意只做"复用的那一块",因为剩下两件事(投递路由 + 回调效果)需要先定一个产品/授权问题:外部管家受众是否可以让 LoopX 做这次持久写入。先把它显式写进 RFC 再实现,比直接调通一条没人同意过的写路径更负责。
验证:
tests/control_plane_ts/action_review_plan.test.ts新增用例通过(该套件在本机有 325 个与 PostgreSQL/CLI 环境相关的前置失败,未改动的 checkout 上同样 325 个);docs governance 与loopx canary premerge --from-git-diff通过。English verdict: APPROVE