经典视图读不到本人生成的排产版本(看板显示 #1

Closed
opened 2026-09-29 22:14:09 +08:00 by ssk · 1 comment
Owner

摘要

计划员在界面上排出方案后,交期看板 / 甘特 / 负荷 / 概览显示"未排产 / 甘特为空",但排产其实成功了——结果写进了该访客自己的世界,而这四个经典视图读的是部署公共世界。

复现(隔离环境,真浏览器 + 真 Pi 兜底)

环境:免登录(APS_AUTH_ENABLED=0)+ APS_SEED_PACK + features.fallback=true,真 Edge 浏览器
操作:界面点击「生成交期优先试排方案」

结果:

兜底运行痕迹:data/fallback/fb-20260929-215339-5a96f9(Pi 确实运行)
产出:world-personal-332455442064162041.json  versions=1  vl=10  wo=72
同一会话读:
  /api/world/summary → {"hasVersion": false}
  /api/world/due     → 10 行,promisedEnd 全空
  /api/world/gantt   → workOrders=0
  /api/flex/gantt    → 正常显示该版本        ← 柔性侧视图是对的
公共世界 world.json → versions=0

对照(同一访客身份直接调 API):POST /api/flex/schedule(engine=OPTIMIZE、sortMode=CP_SAT)成功产出 FV20260929-001(10 虚拟产线 / 72 工单,validation.valid=true),随后同会话四个 /api/world/* 依然 hasVersion:false。

期望行为

按 docs/architecture/auth-tenancy.md 与 docs/architecture/overview.md:

  • 世界按 (tenant_uuid, owner_user_id / project) 分片,世界文件使用命名空间路径;
  • 视图是执行结果的投影(EXE → ENG → VIEW → UI);
  • 因此读路径与写路径必须落在同一份世界:某人排出方案后,他自己的四个经典视图应当能看到该版本;其他人不应看到。

代码定位(供参考)

server/gateway/app.py:1342-1360  四个 /api/world/* 路由直接 get_store().data,不接身份
server/state/store.py:255        get_store() = _scoped_store(*_current_world_key())
server/state/store.py            _current_world_key(): key = get_project_store().active_world_key();
                                 仅当 key ∈ {default,"",__personal__} 且 identity.user_id 非空时映射为 personal-<user_id>
server/state/store.py            switch_store(): 显式切换世界(app.py:721 等 5 处调用)

探针显示:在视图路由的执行上下文中 get_identity() 为 user_id=0 / platform,key 停在 default → 读公共世界。

历史:接口定义于 a91f592(first commit,单世界时代);分片隔离引入于 ef7256f,当时未迁移这四个视图。前端至今仍在使用:apps/web/src/api/client.ts:98/100/102/104。

待确认(需要产品/架构口径)

  1. 这四个接口的语义应为"读当前用户/项目的世界"(与设计文档一致),还是"读部署公共世界"?
  2. 生产认证态(APS_AUTH_ENABLED=1 + 已选项目)下是否同样出现?本 issue 的复现限于免登录开发态。

建议修法

  • A(推荐):四个视图路由按请求身份解析世界,与其它业务路由同源(统一 scoped-store 依赖或显式切换到当前 active project)。接口名与返回结构不变,前端零改动。
  • B:前端改走柔性侧接口(/api/flex/* 本已正确分片),经典视图降级为历史视图。

缺失的门禁测试(本 issue 的一部分)

当前测试只覆盖"甲看不到乙的项目/会话",没有覆盖"甲排出方案后,甲自己的看板能看到结果"。建议新增:

访客 A:排出方案 → /api/world/summary.hasVersion == True
                   且 /api/world/due 至少一行 promisedEnd 非空
访客 B:同样四个视图 hasVersion == False(不得看到 A 的版本)

装置可复用 tests/golden/test_anonymous_visitor_isolation.py 的 open_app + X-APS-Visitor-ID 写法。

验收

  • 新增门禁用例通过;
  • 不回归:test_anonymous_visitor_isolation、test_world_due_view、test_action_result_narrative、test_contract_sync;
  • 端到端复述一次并截图留档。
## 摘要 计划员在界面上排出方案后,**交期看板 / 甘特 / 负荷 / 概览**显示"未排产 / 甘特为空",但排产其实成功了——结果写进了该访客自己的世界,而这四个经典视图读的是部署公共世界。 ## 复现(隔离环境,真浏览器 + 真 Pi 兜底) ```text 环境:免登录(APS_AUTH_ENABLED=0)+ APS_SEED_PACK + features.fallback=true,真 Edge 浏览器 操作:界面点击「生成交期优先试排方案」 ``` 结果: ```text 兜底运行痕迹:data/fallback/fb-20260929-215339-5a96f9(Pi 确实运行) 产出:world-personal-332455442064162041.json versions=1 vl=10 wo=72 同一会话读: /api/world/summary → {"hasVersion": false} /api/world/due → 10 行,promisedEnd 全空 /api/world/gantt → workOrders=0 /api/flex/gantt → 正常显示该版本 ← 柔性侧视图是对的 公共世界 world.json → versions=0 ``` 对照(同一访客身份直接调 API):`POST /api/flex/schedule`(engine=OPTIMIZE、sortMode=CP_SAT)成功产出 `FV20260929-001`(10 虚拟产线 / 72 工单,`validation.valid=true`),随后同会话四个 `/api/world/*` 依然 `hasVersion:false`。 ## 期望行为 按 `docs/architecture/auth-tenancy.md` 与 `docs/architecture/overview.md`: - 世界按 `(tenant_uuid, owner_user_id / project)` 分片,世界文件使用命名空间路径; - 视图是执行结果的投影(`EXE → ENG → VIEW → UI`); - 因此**读路径与写路径必须落在同一份世界**:某人排出方案后,他自己的四个经典视图应当能看到该版本;其他人不应看到。 ## 代码定位(供参考) ```text server/gateway/app.py:1342-1360 四个 /api/world/* 路由直接 get_store().data,不接身份 server/state/store.py:255 get_store() = _scoped_store(*_current_world_key()) server/state/store.py _current_world_key(): key = get_project_store().active_world_key(); 仅当 key ∈ {default,"",__personal__} 且 identity.user_id 非空时映射为 personal-<user_id> server/state/store.py switch_store(): 显式切换世界(app.py:721 等 5 处调用) ``` 探针显示:在视图路由的执行上下文中 `get_identity()` 为 `user_id=0 / platform`,key 停在 `default` → 读公共世界。 历史:接口定义于 `a91f592`(first commit,单世界时代);分片隔离引入于 `ef7256f`,当时未迁移这四个视图。前端至今仍在使用:`apps/web/src/api/client.ts:98/100/102/104`。 ## 待确认(需要产品/架构口径) 1. 这四个接口的语义应为"读当前用户/项目的世界"(与设计文档一致),还是"读部署公共世界"? 2. 生产认证态(`APS_AUTH_ENABLED=1` + 已选项目)下是否同样出现?本 issue 的复现限于免登录开发态。 ## 建议修法 - **A(推荐)**:四个视图路由按请求身份解析世界,与其它业务路由同源(统一 scoped-store 依赖或显式切换到当前 active project)。接口名与返回结构不变,前端零改动。 - **B**:前端改走柔性侧接口(`/api/flex/*` 本已正确分片),经典视图降级为历史视图。 ## 缺失的门禁测试(本 issue 的一部分) 当前测试只覆盖"甲看不到乙的项目/会话",**没有覆盖"甲排出方案后,甲自己的看板能看到结果"**。建议新增: ```text 访客 A:排出方案 → /api/world/summary.hasVersion == True 且 /api/world/due 至少一行 promisedEnd 非空 访客 B:同样四个视图 hasVersion == False(不得看到 A 的版本) ``` 装置可复用 `tests/golden/test_anonymous_visitor_isolation.py` 的 `open_app` + `X-APS-Visitor-ID` 写法。 ## 验收 - 新增门禁用例通过; - 不回归:`test_anonymous_visitor_isolation`、`test_world_due_view`、`test_action_result_narrative`、`test_contract_sync`; - 端到端复述一次并截图留档。
Author
Owner

撤回:本 issue 的根因判断有误

先撤回,避免按错误线索改动代码。

原判断:四个 /api/world/* 视图读的是部署公共世界,与排产写入的个人世界不一致("排完看不见")。

实测反证(在真实请求上下文里插桩,TestClient + X-APS-Visitor-ID):

GET /api/world/due 请求上下文:
  user_id=506447492639470595  tenant=platform
  store = world-personal-506447492639470595.json      ← 解析到该访客自己的世界
  world_key = ('platform','personal-506447492639470595')   ← default→personal 映射生效

POST /api/flex/schedule 之后再 GET /api/world/due:
  同一个世界文件,flexScheduleVersions = 1            ← 视图读得到刚排的版本

磁盘上世界文件只有一个(未生成 world.json)

结论:视图与排产落在同一份世界,不存在作用域错配。本 issue 的因果链不成立。

被误读的信号:world_summary 的 hasVersion 判的是经典版本表(scheduleVersions),而排产写的是柔性版本表(flexScheduleVersions)——只有柔性版本时它本来就返回 false。这是指标口径问题,与作用域无关。我此前把它当成了"看板读错世界"的证据。

仍然待查(另行跟进,不在本 issue 范围):只有柔性版本时,/api/world/due 的 promisedEnd 是否应按 flexVirtualLines[].plannedEnd 填充。主线 ce2fac9 已引入柔性通道回退(_latest_flex_version() / _flex_promised_by_order()),需确认它覆盖的情形。

处置:关闭本 issue。如需继续跟进,请按上面"仍然待查"另开一条,判据用柔性可见性(/api/flex/gantt 有版本、due.promisedEnd 是否符合预期),不要再用 world_summary.hasVersion。

## 撤回:本 issue 的根因判断有误 先撤回,避免按错误线索改动代码。 **原判断**:四个 `/api/world/*` 视图读的是部署公共世界,与排产写入的个人世界不一致("排完看不见")。 **实测反证**(在真实请求上下文里插桩,`TestClient` + `X-APS-Visitor-ID`): ```text GET /api/world/due 请求上下文: user_id=506447492639470595 tenant=platform store = world-personal-506447492639470595.json ← 解析到该访客自己的世界 world_key = ('platform','personal-506447492639470595') ← default→personal 映射生效 POST /api/flex/schedule 之后再 GET /api/world/due: 同一个世界文件,flexScheduleVersions = 1 ← 视图读得到刚排的版本 磁盘上世界文件只有一个(未生成 world.json) ``` 结论:视图与排产**落在同一份世界**,不存在作用域错配。本 issue 的因果链不成立。 **被误读的信号**:`world_summary` 的 `hasVersion` 判的是**经典版本表**(`scheduleVersions`),而排产写的是**柔性版本表**(`flexScheduleVersions`)——只有柔性版本时它本来就返回 `false`。这是指标口径问题,与作用域无关。我此前把它当成了"看板读错世界"的证据。 **仍然待查(另行跟进,不在本 issue 范围)**:只有柔性版本时,`/api/world/due` 的 `promisedEnd` 是否应按 `flexVirtualLines[].plannedEnd` 填充。主线 `ce2fac9` 已引入柔性通道回退(`_latest_flex_version()` / `_flex_promised_by_order()`),需确认它覆盖的情形。 **处置**:关闭本 issue。如需继续跟进,请按上面"仍然待查"另开一条,判据用柔性可见性(`/api/flex/gantt` 有版本、`due.promisedEnd` 是否符合预期),不要再用 `world_summary.hasVersion`。
ssk closed this issue 2026-09-29 22:23:14 +08:00
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: Industry/aps-agent#1
No description provided.