feat(frontend+api): 因子清单加批次分割线——台账反查出生批次分组显示 [vps]
CI/CD / test (push) Successful in 45s
CI/CD / nas-deploy (push) Successful in 31s
CI/CD / nas-verify (push) Successful in 9s

用户复验追加小需求:21 条因子平铺看不出批次归属。

- 后端:hypotheses_list 每条 factor 附 batch(台账 registered
  name→job 映射,新→旧先见者胜;查不到=null 归「早期」);
  清单排序改按批次新→旧,无批次垫底
- 前端:批次切换处插虚线行「── 第 MM-DD HH:MM 批 · job-… ──」,
  无批次段前插「── 早期(无批次记录) ──」

测试:factors[0].batch.jobId 断言;后端 28 绿+前端 76 绿+构建过
This commit is contained in:
2026-10-09 13:01:22 +08:00
parent c04bd8dd0c
commit 7f512f1461
5 changed files with 39 additions and 8 deletions
@@ -123,3 +123,4 @@ spec 逐节拍板 → writing-plans 出实施计划 → 分域派单:②权重
- 2026-10-09:LLM 链两连修上产(端点错配 85d14a1 + client 全流式化 acc273b,QA 同款);**生产首批 origin:decomposer 因子落册**(高管增持卡,D7 零实绩破局);终局诊断=响应孤儿化(服务端成功+浏览器断连无感知),治本=**分解 job 化**(QA 任务字典范式:202 立返+后台任务+卡片 decomposeJob 轮询+decompose_jobs 台账表)——§7 件④地基落,总设计 §13 P5.3 同步;生产全程验证通过(job-20261009101513:202 秒回→3 轮 2m43s→8 因子 0 失败);件①扩为三件套(产物清单+批次历史+轮次进度,QA 调研:progress 字段结构/tasks-list 端点照抄,currentRound 摆设我们用真轮数,WS 直播归 D2)
- 2026-10-09 晚:**D7 真实现落地**(8823 体验稿逐条过审拍板后动工):件①1a=列表项内联 `factors` 清单(registry hypothesis 血统反查:名/表达式/origin/落成日;卡片单因子名 factorId 前端退役);1b=新端点 `GET /pipeline/hypotheses/{id}/decompose-jobs`(台账新→旧,QA tasks/list 形状)+卡片折叠区按需拉取;1c=`run_decompose` 每轮 `on_progress` 回调→内存 job `progress`(currentRound/totalRounds/passed/regen/message;台账仍只记起跑+终态),worker 协议改收 job_id;件②=factors 端点带 origin/hypothesis+工厂「来源」列(⚡decomposer/✍manual 徽标+回链假设池);件③=todos 聚合加 queued/data_check 卡琥珀待办(分解转 building 即消行,人工卡点②显性化);件④=前端一次性结果弹层退役,结果持久落卡。测试=后端 TestD7Visibility 六件+前端三件套三测
- 2026-10-09 深夜:**D7 VPS 验收反馈四条处理**(877723a 首班上产后用户实测):①因子详情页落地——`GET /pipeline/factors/{name}/detail`(出生档案:origin/来源假设/出生批次=台账含该名最新 job/表达式/落成日)+前端 `FactorDetail.vue`(`/pipeline/factors/:name` 路由),卡片因子点名与工厂行点击均进详情;②总览无变化=数据驱动非缺陷(唯一卡已 building,无 queued 卡时琥珀项不出现——落新卡即现;地铁图属 D1-D6);③工厂来源列改 min-width 200+徽标/回链竖排;④轮次进度起跑即报「第 1/3 轮 · 生成候选中…」(初代生成期不带轮次字样的体验缺陷)
- 2026-10-09 深夜补:件①1a 因子清单加**批次分割线**(用户复验追加)——每条因子附出生批次(台账 registered 反查 name→job 映射,查不到=null 归「早期」),清单按批次新→旧分组,批间虚线行「── 第 MM-DD HH:MM 批 · job-… ──」
+1
View File
@@ -34,6 +34,7 @@ export interface HypothesisFactor {
origin: string
createdAt: string | null
status: string
batch?: { jobId: string; startedAt: string } | null // 出生批次(台账反查,null=早期/无记录)
}
export type FactorStatus = 'incubating' | 'assessable' | 'promoted' | 'decaying' | 'retired'
+19 -6
View File
@@ -250,12 +250,22 @@ function progText(h: Hypothesis): string {
<span class="hint">点名进工厂 · 血统=registry 反查</span>
</div>
<div v-if="openFac[h.id]" class="sec-body">
<div v-for="f in h.factors" :key="f.name" class="fac-row">
<span class="fac-name" title="看出生档案"
@click="router.push(`/pipeline/factors/${f.name}`)">{{ f.name }}</span>
<code class="fac-expr">{{ f.expression }}</code>
<span class="fac-date">{{ f.createdAt }}</span>
</div>
<template v-for="(f, i) in h.factors" :key="f.name">
<!-- 批次分割线:批次切换处插行;首条无批次而清单非空=早期段 -->
<div v-if="f.batch && (i === 0 || h.factors[i - 1].batch?.jobId !== f.batch.jobId)"
class="fac-batch">
── 第 {{ f.batch.startedAt.slice(5, 16).replace('T', ' ') }} 批 · {{ f.batch.jobId }} ──
</div>
<div v-else-if="!f.batch && (i === 0 || h.factors[i - 1].batch)" class="fac-batch early">
── 早期(无批次记录) ──
</div>
<div class="fac-row">
<span class="fac-name" title="看出生档案"
@click="router.push(`/pipeline/factors/${f.name}`)">{{ f.name }}</span>
<code class="fac-expr">{{ f.expression }}</code>
<span class="fac-date">{{ f.createdAt }}</span>
</div>
</template>
</div>
</div>
<!-- 件①1b:分解批次历史(decompose_jobs 台账,按需拉取) -->
@@ -359,6 +369,9 @@ function progText(h: Hypothesis): string {
.fac-name:hover { text-decoration: underline; }
.fac-expr { flex: 1; min-width: 0; font-family: var(--mono); font-size: 10px; color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.fac-date { color: var(--text-3); font-size: 10px; white-space: nowrap; }
.fac-batch { font-family: var(--mono); font-size: 9.5px; color: var(--cyan-dim); border-top: 1px dashed rgba(0, 184, 204, 0.35); margin-top: 4px; padding-top: 3px; }
.fac-batch:first-child { border-top: none; margin-top: 0; padding-top: 0; }
.fac-batch.early { color: var(--text-3); border-top-color: var(--border); }
.batch { border: 1px solid var(--border); border-radius: var(--r-sm); margin-bottom: 5px; }
.batch-h { display: flex; align-items: center; gap: 7px; padding: 5px 8px; cursor: pointer; font-size: 11px; flex-wrap: wrap; }
.batch-h:hover { background: var(--cyan-soft); }
+14 -1
View File
@@ -955,9 +955,22 @@ def hypotheses_list() -> dict:
"status": e.get("status")})
except Exception: # noqa: BLE001 registry 读失败不拖垮卡片列表
logger.warning("hypotheses factors 反查失败,如实空清单", exc_info=True)
by_hyp = {}
# 批次归属(验收追加:清单加批次分割线)——台账新→旧先见者胜,
# 每条因子附 birth batch;台账查不到(超 limit/远古)=None 归「早期」
batch_of: dict[tuple[str, str], dict[str, Any]] = {}
for hyp_id in by_hyp:
for job in pipeline_store.list_decompose_jobs(_pipeline_db(), hyp_id):
for c in job.get("registered") or []:
batch_of.setdefault((hyp_id, c["name"]),
{"jobId": job["jobId"],
"startedAt": job["startedAt"]})
for it in items:
fs = by_hyp.get(it["id"]) or []
fs.sort(key=lambda f: f["createdAt"] or "", reverse=True)
for f in fs:
f["batch"] = batch_of.get((it["id"], f["name"]))
fs.sort(key=lambda f: (f["batch"] or {}).get("startedAt") or "", # noqa: E501 批次新→旧,无批次垫底
reverse=True)
it["factors"] = fs
return {"items": items}
+4 -1
View File
@@ -382,13 +382,16 @@ class TestD7Visibility:
_, queue = fake_llm
queue.append(DECOMP_GOOD)
hyp = _seed_card(client)
assert _wait_job(client, hyp)["status"] == "completed"
job = _wait_job(client, hyp)
assert job["status"] == "completed"
items = client.get("/api/v1/pipeline/hypotheses").json()["items"]
item = next(i for i in items if i["id"] == hyp)
assert [f["name"] for f in item["factors"]] == ["llm_mom20"]
assert item["factors"][0]["origin"] == "decomposer"
assert item["factors"][0]["expression"].startswith("cs_rank")
assert item["factors"][0]["createdAt"] # 落成日=首版 effective_from
# 批次归属(分割线数据源):registered 含该名的最新 job
assert item["factors"][0]["batch"]["jobId"] == job["jobId"]
def test_progress_reports_rounds_while_running(self, client, fake_llm,
tmp_path, monkeypatch):