feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通

- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
    版本化迁移运行器(M1~M13)
  - 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
    Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
    新增 tlusty_status/synspec_status 半失败阶段守卫
  - 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
    校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
  - 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
    透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
  - 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
  - dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
  - docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
This commit is contained in:
fmq
2026-08-06 20:51:21 +08:00
parent cd370d88e7
commit d16b3d3cdc
61 changed files with 10268 additions and 5881 deletions
+13 -9
View File
@@ -159,10 +159,10 @@ ND=50,NLAMBD=3,VTB=2.,ISPODF=1,DDNU=50.,CNU1=6.,[CHMAX=..][,ITEK=..],NITER=<阶
### 3.3 synspec 配置(fort.55 + 谱线表)
```
fort.55 控制卡: 波长窗/展宽/截断(由 workflow YAML 的 synspec: 块或代码默认配置)
fort.55 控制卡: 波长窗/展宽/截断(由 workflow YAML 的 `synspec_input:` 块或代码默认配置)
谱线表 fort.19: data 下 gf 谱线数据(含 C/N/O 线)
```
- 代码默认波长窗为 **14001410 Å**`SynspecConfig` 默认),实际使用通常配置到
- 代码默认波长窗为 **14001410 Å**`SynspecInput` 默认),实际使用通常配置到
目标波段(如 3000-7000Å 光学波段,覆盖 C II 4267、C III 4647 等)。
- 大气来自 nl 阶段的 `.7`(复制为 fort.8)。
@@ -245,7 +245,8 @@ fort.55 控制卡: 波长窗/展宽/截断(由 workflow YAML 的 synspec: 块
> **走种子步进链时**`seed` 指向邻居 `.7``stages` 里没有 `lte`,而是
> `seed_nc``ltgray=F` 热启动,NITER=20)→ `nl`。
> 注意:`conv.json` 不含 `seed_step_used` / `coldfail_backup` 布尔字段(旧版字段已移除);
> 收敛手段归因由服务端 `tasks` 表的 `task_type`cold_run / seed_step)聚合统计
> 收敛手段归因由服务端 `tasks` 表的 `tlusty_strategies[0]`cold_run / seed_step)聚合统计
> Phase 6 起 `task_type` 列已删除,策略链首项即当前执行策略的权威快照)。
每阶段记录:`converged`(是否收敛)、`max_relc`(最大相对变化)、
`worst_depth`(最差深度点)、`last_iter`(迭代次数)、`n_depths`(深度点数)、
@@ -262,11 +263,11 @@ runner 在每个阶段的循环开始/结束处计时,conv.json 里每个 stag
早期 `grid_status.json` 文件已废弃;当前状态统计由服务端 API 实时查询 DB 提供:
- `GET /api/workflows`:各工作流内联进度(total/converged/failed/running…)
- `GET /api/workflows`:各工作流内联进度(total/completed/failed/running…`completed` 由 7c 改名自 `converged`
- `GET /api/workflows/:name/stats`:单工作流统计(含 `cold_run_converged` / `seed_step_converged`
- `GET /api/workflows/:name/points`:逐点列表(可过滤 status/method/wave/q,分页)
- `GET /api/workflows/:name/progress`:进度-时间序列曲线
- 种子步进命中数 = `tasks``task_type='seed_step'``status='completed'` 的聚合
- 种子步进命中数 = `tasks``json_extract(tlusty_strategies,'$[0]')='seed_step'``status='completed'` 的聚合
前端 Dashboard 直接消费以上端点渲染(首页卡片、详情页概览/点表/平行集合分析图)。
@@ -291,14 +292,17 @@ grid:
logn: [-4, -2, -1]
logo: [-4, -2, -1]
# 共 4*2*2*3*3*3 = 432 个点
tlusty:
tlusty_stage: # TLUSTY 阶段独立配置(enabled/policy/strategies
enabled: true
policy: skip_converged
strategies: ["cold_run", "seed_step"] # 策略链:冷启动失败回退种子步进
synspec:
synspec_stage: # SYNSPEC 阶段独立配置(enabled/policy/strategies
enabled: true
wstart: 3000
wend: 7000
policy: skip_converged
strategies: ["standard"]
synspec_input: # SYNSPEC 数值参数(fort.55 波长范围等)
wstart: 3000.0
wend: 7000.0
```
### 第2步:创建并启动工作流(HTTP API / Dashboard
+43 -38
View File
@@ -110,32 +110,32 @@
pub task_id: Uuid,
pub point_name: String,
pub params: GridPointParams,
pub task_type: TaskType, // ColdRun | SeedStep(兼容字段 = strategies[0]
pub seed_point_name: Option<String>, // 步进种子点名称(如适用)
pub timeout_sec: u64,
pub workflow_name: Option<String>, // 多工作流分区键
pub wave: i32, // 难度波次
pub tlusty_config: EngineStageConfig, // TLUSTY 阶段配置(enabled/policy/strategies
pub synspec_config: EngineStageConfig, // SYNSPEC 阶段配置
pub tlusty_config: PhaseConfig, // TLUSTY 阶段配置(enabled/policy/strategies
pub synspec_config: PhaseConfig, // SYNSPEC 阶段配置
pub synspec_params: Option<serde_json::Value>, // SYNSPEC 数值参数(波长范围等)
pub atmosphere_ref: Option<String>, // 显式大气来源点(仅 SYNSPEC-only 场景)
}
```
`EngineStageConfig`(阶段独立配置,见 `task_engine_decoupling_design.md §3`):
`PhaseConfig`(阶段独立配置,见 `task_engine_decoupling_design.md §3`Phase 7b 由 `EngineStageConfig` 改名):
```rust
pub struct EngineStageConfig {
pub struct PhaseConfig {
pub enabled: bool,
pub policy: String, // skip_converged | force_recompute | skip_failed
pub policy: ResumePolicy, // skip_converged | force_recompute | skip_failed
pub strategies: Vec<String>, // 策略链:["cold_run","seed_step"],失败回退弹首项
}
```
- **TypeScript 类型声明**:
```typescript
export type TaskType = 'cold_run' | 'seed_step';
> Phase 6 起 **无 `task_type` 字段**——执行链由 `tlusty_config.strategies[0]` 派生。
> 旧 payload 若仍携带 `task_type` 键会被 serde 忽略(未知字段)。
export interface EngineStageConfig {
- **TypeScript 类型声明**Phase 6 起无 `task_type`:
```typescript
export interface PhaseConfig {
enabled: boolean;
policy: string;
strategies: string[];
@@ -145,13 +145,12 @@
task_id: string;
point_name: string;
params: GridPointParams;
task_type: TaskType;
seed_point_name?: string | null;
timeout_sec: number;
workflow_name?: string | null;
wave: number;
tlusty_config: EngineStageConfig;
synspec_config: EngineStageConfig;
tlusty_config: PhaseConfig;
synspec_config: PhaseConfig;
synspec_params?: Record<string, unknown> | null;
atmosphere_ref?: string | null;
}
@@ -172,7 +171,7 @@ Worker 节点向服务端上报的任务计算结果。
pub params: Option<GridPointParams>,
pub node_id: String,
pub status: TaskStatus, // Pending | Running | Completed | Failed | Timeout
pub converged: bool,
pub result_valid: bool, // 7b 改名(原 converged):本次结果是否可用(大气收敛/管线成功双义)
pub max_relc: Option<f64>,
pub atmosphere_has_nan: bool,
pub elapsed_sec: f64,
@@ -202,7 +201,7 @@ Worker 节点向服务端上报的任务计算结果。
params?: GridPointParams;
node_id: string;
status: TaskStatus;
converged: boolean;
result_valid: boolean; // 7b 改名(原 converged
max_relc?: number | null;
atmosphere_has_nan: boolean;
elapsed_sec: number;
@@ -311,11 +310,12 @@ Worker 节点向服务端上报的任务计算结果。
export interface WorkflowListStats {
total: number;
converged: number;
completed: number; // 7c:由 converged 改名(状态值 'completed'
failed: number;
running: number;
cold_run_converged: number;
seed_step_converged: number;
synspec_converged: number;
}
export interface WorkflowItem {
@@ -550,9 +550,10 @@ Worker 节点向服务端上报的任务计算结果。
"logn": -2.0,
"logo": -2.0
},
"task_type": "cold_run",
"seed_point_name": null,
"timeout_sec": 7200
"timeout_sec": 7200,
"tlusty_config": { "enabled": true, "policy": "skip_converged", "strategies": ["cold_run", "seed_step"] },
"synspec_config": { "enabled": true, "policy": "skip_converged", "strategies": ["standard"] }
}
}
```
@@ -656,7 +657,7 @@ Worker 节点向服务端上报的任务计算结果。
- **请求格式**: `multipart/form-data`
- Part `report`: 旧版 `conv.json` 的**原文 JSON**(映射为 `ModelSummary`,服务端解析出 `name` / `params` / `converged` / `final_max_relc`
- Part `seed_file`: 二进制数据(`.7` 大气种子文件;收敛点必传)
- Part `success_method` *(可选)*: 文本 `cold_run` / `seed_step`。由 `tools/import_results` 依据旧 `conv.json` 的 stages 是否含 `seed_nc` 判定后设置,决定导入点最终归因(缺省按 seed_step 统计)
- Part `tlusty_success_method` *(可选)*: 文本 `cold_run` / `seed_step`。由 `tools/import_results` 依据旧 `conv.json` 的 stages 是否含 `seed_nc` 判定后设置,写入导入点大气归因 `tlusty_success_method`(缺省按 seed_step 统计)
- **Query 参数**: `workflow`(可选,默认 `imported`):目标工作流名,种子导入到该工作流的 `grid_points`。
- **命名保真**: `point_name` 取旧 `conv.json` 的 `name` 字段(源精度真名,如 `t20000_g5.0_...`),**逐字符**落库(磁盘目录、`grid_points.name`、`seeds.point_name`),与旧版 Python `gen_input5.model_name` 完全一致。
- **幂等**: `ON CONFLICT DO NOTHING` upsert `grid_points`、`conv.json` 与 `.7` 原子覆盖写,可重复运行。
@@ -810,10 +811,11 @@ Worker 节点向服务端上报的任务计算结果。
"pending": 210,
"queued": 60,
"running": 32,
"converged": 260,
"completed": 260,
"failed": 10,
"cold_run_converged": 200,
"seed_step_converged": 60
"seed_step_converged": 60,
"synspec_converged": 245
}
}
```
@@ -1101,12 +1103,12 @@ Worker 节点向服务端上报的任务计算结果。
"pending": 120,
"queued": 40,
"running": 8,
"converged": 261,
"completed": 261,
"failed": 3,
"cold_run_converged": 220,
"seed_step_converged": 41,
"waves": [
{ "wave": 0, "total": 108, "converged": 108, "failed": 0 }
{ "wave": 0, "total": 108, "completed": 108, "failed": 0 }
],
"avg_point_sec": 740.5,
"eta_sec": 9620.0
@@ -1114,10 +1116,10 @@ Worker 节点向服务端上报的任务计算结果。
}
```
> 收敛手段归因仅 `cold_run_converged` / `seed_step_converged` 两字段;**无独立
> `imported_converged`**——历史导入点统一按 seed_step 途径计入(`success_method` 语义见 §8.8)。
> `imported_converged`**——历史导入点统一按 seed_step 途径计入(大气归因 `tlusty_success_method` 语义见 §8.8)。
> `avg_point_sec` = `AVG(COALESCE(tasks.elapsed_sec, created_at→completed_at 时间戳差))`——
> 优先用 Worker 回报的精确墙钟(不含排队等待),历史无 `elapsed_sec` 的行回退时间戳差近似;
> `eta_sec` = `avg_point_sec × (total - converged - failed) ÷ 在线节点总槽位`(并发感知;
> `eta_sec` = `avg_point_sec × (total - completed - failed) ÷ 在线节点总槽位`(并发感知;
> 无在线节点按串行兜底);无历史数据时二者为 `null`。
---
@@ -1132,8 +1134,8 @@ Worker 节点向服务端上报的任务计算结果。
| 参数 | 取值 | 默认 |
| :--- | :--- | :--- |
| `status` | `pending`/`queued`/`running`/`converged`/`failed` | 不过滤 |
| `method` | `cold_run`/`seed_step`(白名单**不含** `imported`,传入即 `400` | 不过滤 |
| `status` | `pending`/`queued`/`running`/`completed`/`failed`(旧值 `converged` 仍作兼容别名接受,服务端归一化为 `completed` | 不过滤 |
| `method` | `cold_run`/`seed_step`映射 `tlusty_success_method``synspec_only`(光谱专用点:`tlusty_success_method IS NULL AND synspec_success_method IS NOT NULL`)。白名单**不含** `imported`,传入即 `400` | 不过滤 |
| `wave` | 整数波次 | 不过滤 |
| `q` | 点名子串(LIKE 通配符已转义) | 不过滤 |
| `sort` | `wave`/`teff`/`max_relc`/`attempts`/`last_completed_at` | `wave` |
@@ -1154,11 +1156,11 @@ Worker 节点向服务端上报的任务计算结果。
"teff": 60000.0, "logg": 5.0, "loghe": -2.0,
"logc": -4.0, "logn": -4.0, "logo": -4.0,
"cno_sum": -12.0, "wave": 0,
"status": "converged",
"success_method": "seed_step",
"status": "completed",
"tlusty_success_method": "seed_step",
"synspec_success_method": "standard",
"attempt_count": 2,
"last_max_relc": 0.00043,
"last_task_type": "seed_step",
"seed_point_name": "t60000_g5.0_he2_c-4_n-4_o-4",
"node_id": "node-a1b2",
"last_completed_at": "2026-07-30 11:12:00",
@@ -1190,7 +1192,6 @@ Worker 节点向服务端上报的任务计算结果。
"attempts": [
{
"task_id": "uuid",
"task_type": "cold_run",
"seed_point_name": null,
"status": "failed",
"max_relc": 954000.0,
@@ -1198,9 +1199,12 @@ Worker 节点向服务端上报的任务计算结果。
"node_id": "node-a1b2",
"error_message": "nl stage diverged",
"created_at": "2026-07-30 09:00:00",
"completed_at": "2026-07-30 09:30:00"
"completed_at": "2026-07-30 09:30:00",
"elapsed_sec": 1800.0,
"failed_stage": "tlusty",
"summary_json": "{... ModelSummary ...}"
},
{ "task_type": "seed_step", "status": "completed", "...": "第二次尝试(救回)" }
{ "seed_point_name": "t60000_...", "status": "completed", "failed_stage": null, "...": "第二次尝试(救回)" }
],
"conv": {
"converged": true,
@@ -1242,7 +1246,7 @@ Worker 节点向服务端上报的任务计算结果。
"hours": 24,
"series": [
{ "ts": "2026-07-31 08:00:00", "total": 432, "pending": 120, "queued": 40,
"running": 8, "converged": 261, "failed": 3 }
"running": 8, "completed": 261, "failed": 3 }
],
"rate_per_hour": 12.5,
"now": "2026-07-31T09:00:00Z",
@@ -1253,9 +1257,9 @@ Worker 节点向服务端上报的任务计算结果。
}
```
> `series` 超 300 条自动降采样(首末点保留);`rate_per_hour` = 窗口首末
> converged 增量 ÷ 时长(快照不足 2 条为 null);`now` = 服务端当前 UTC 时刻(前端锚定曲线右缘);
> completed 增量 ÷ 时长(快照不足 2 条为 null);`now` = 服务端当前 UTC 时刻(前端锚定曲线右缘);
> `done_rate_per_hour` = 终态完成速率(用于 ETA);`rate_span_hours` = 速率统计实际时间跨度;
> `stalled_minutes` = 终态数(converged+failed)最后一次增长至窗口末端的分钟数(前端 >10 分钟触发停滞预警)。
> `stalled_minutes` = 终态数(completed+failed)最后一次增长至窗口末端的分钟数(前端 >10 分钟触发停滞预警)。
---
@@ -1277,11 +1281,12 @@ Worker 节点向服务端上报的任务计算结果。
"updated_at": "2026-07-30 11:00:00",
"stats": {
"total": 432,
"converged": 261,
"completed": 261,
"failed": 3,
"running": 8,
"cold_run_converged": 220,
"seed_step_converged": 41
"seed_step_converged": 41,
"synspec_converged": 248
}
}
]
+1 -1
View File
@@ -86,7 +86,7 @@ stateDiagram-v2
```
> 状态值与 [`GridPointStatus`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs#L237) 一一对应:
> `pending` → `queued` → `running` → `converged` / `failed`。
> `pending` → `queued` → `running` → `completed` / `failed``completed` 由 7c 改名自 `converged`,旧值仍作兼容别名解析)
> Requeue 把 `running` 直接重置回 `pending`(不经过 `queued`),由调度器下一轮重新推入队列。
---
+23 -10
View File
@@ -38,14 +38,14 @@ erDiagram
integer wave
string status
integer attempt_count
string success_method
string tlusty_success_method
string synspec_success_method
double last_elapsed_sec
}
TASKS {
string task_id PK
string point_name
string node_id
string task_type
string seed_point_name
string status
double max_relc
@@ -64,6 +64,8 @@ erDiagram
text synspec_strategies
string atmosphere_ref
double elapsed_sec
string failed_stage
text summary_json
}
NODES {
string node_id PK
@@ -80,7 +82,6 @@ erDiagram
string node_id PK
string token_hash
datetime issued_at
integer revoked
string raw_token_pending
}
SEEDS {
@@ -103,7 +104,7 @@ erDiagram
integer pending
integer queued
integer running
integer converged
integer completed
integer failed
}
end
@@ -143,9 +144,13 @@ erDiagram
- `teff`, `logg`, `loghe`, `logc`, `logn`, `logo` (`REAL NOT NULL`)6 维物理参数。
- `cno_sum` (`REAL NOT NULL`)CNO 丰度之和(调度排序用)。
- `wave` (`INTEGER NOT NULL DEFAULT 0`):按 cno_sum 分组的批次波次(调度优先级用)。
- `status` (`VARCHAR(32)`)`pending` / `queued` / `running` / `converged` / `failed`
- `status` (`VARCHAR(32)`)`pending` / `queued` / `running` / `completed` / `failed`7c 由 `converged` 改名,M9 迁移)
- `attempt_count` (`INTEGER`):失败重试计数(仅观测用)。
- `success_method` (`VARCHAR(32)`)收敛时的成功手段 (`cold_run` 冷启动成功 / `seed_step` 种子步进成功 / `imported` 历史导入)。
- `tlusty_success_method` (`VARCHAR(32)`)**大气收敛归因**(P9 拆分)——TLUSTY 阶段以何策略收敛
(`cold_run` 冷启动成功 / `seed_step` 种子步进成功 / 策略名)TLUSTY 禁用(synspec-only)为 NULL。
Phase 6 起由 `tlusty_strategies[0]` 派生。整体归因由消费方派生(`tlusty ?? synspec`)。
- `synspec_success_method` (`VARCHAR(32)`)**光谱收敛归因**Phase 5a)——synspec 阶段以何策略收敛
(如 `standard`);TLUSTY-only 成功为 NULL。解锁"光谱以 standard 等策略收敛了多少点"的统计与过滤。
- `last_elapsed_sec` (`REAL`):最近一次尝试的墙钟耗时(详情页 ETA 估算用,兼容旧数据回退)。
> **多工作流分区(per-workflow partitioning**`grid_points` 与 `task_queue` 均按 `workflow_name` 隔离。
@@ -158,8 +163,8 @@ erDiagram
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 IDUUID)。
- `point_name` (`TEXT NOT NULL`):网格点权威名(源精度,非 params 重推)。
- `node_id` (`TEXT`):执行节点 ID。
- `task_type` (`VARCHAR(32)`):兼容字段,等于 `tlusty_strategies[0]``cold_run` / `seed_step`),供旧节点识别。
- `seed_point_name` (`TEXT`):种子步进时注入的近邻种子点(`GET /api/seed/<name>` 下载依据)。
**双义**SYNSPEC-onlyTLUSTY 关闭)时恒为 NULL(大气来源见 `atmosphere_ref`)。
- `status` (`VARCHAR(32)`)`pending` / `claimed` / `running` / `completed` / `failed` / `timeout`
- `max_relc` (`REAL`):最终最大相对变化(收敛判据量)。
- `atmosphere_has_nan` (`BOOLEAN`):最终大气是否含 >10% NaN 行(无效化标记)。
@@ -172,6 +177,10 @@ erDiagram
`synspec_strategies` (`JSON`)、`atmosphere_ref` (`TEXT`,显式大气来源点)。
回退时弹 `*_strategies` 链首(见 `task_engine_decoupling_design.md §4.2`),policy/策略链取派发时快照。
- `elapsed_sec` (`REAL`):任务墙钟耗时(详情页 ETA 估算优先用此值)。
- `failed_stage` (`TEXT`):失败阶段归因(`"tlusty"` / `"synspec"`;旧节点/旧行 NULL → 服务端兜底按 TLUSTY 归因)。
- `summary_json` (`TEXT`):完整 `ModelSummary` JSON(含 `synspec_rc`/`synspec_error`/`synspec_sec` 与各子步骤摘要);
错误路径为 `{"error": ...}`。**全量保真**Phase 5b 起):逐次 itek 迭代诊断
`itek_history: [{iter, max_relc, n_depths}]`)随 summary_json 落库,与 conv.json 同源。
### 2.4 `nodes` (计算节点心跳与状态表)
- `node_id` (`TEXT PRIMARY KEY`):节点唯一标识(未指定 `DCTS_NODE_ID` 时自动生成 `node-<uuid>`)。
@@ -187,9 +196,11 @@ erDiagram
- `node_id` (`TEXT PRIMARY KEY`):关联 `nodes.node_id`
- `token_hash` (`TEXT NOT NULL`):节点专属 token 的 SHA-256 哈希(**不存明文**)。
- `issued_at` (`DATETIME NOT NULL`):颁发时间。
- `revoked` (`INTEGER NOT NULL DEFAULT 0`):吊销标记(重新颁发 token 时旧行吊销)。
- `raw_token_pending` (`TEXT`):暂存待确认的明文 token(颁发流程过渡用,确认后清除)。
> token 失效靠重发覆盖 `token_hash`(旧 hash 不存在 → 鉴权失败),无独立吊销标记;
> 历史 `revoked` 死列已由迁移 M4 清除(Phase 4)。
### 2.6 `seeds` (种子缓存池表)
全局共享的已收敛大气 `.7` 索引(跨工作流复用,种子步进热启动数据源)。
- `id` (`INTEGER PRIMARY KEY AUTOINCREMENT`)。
@@ -206,12 +217,14 @@ erDiagram
- `id` (`INTEGER PRIMARY KEY AUTOINCREMENT`)。
- `workflow_name` (`TEXT NOT NULL`)。
- `ts` (`DATETIME NOT NULL DEFAULT (datetime('now'))`):采样时间。
- `total` / `pending` / `queued` / `running` / `converged` / `failed` (`INTEGER NOT NULL`):该时刻各状态计数。
- `total` / `pending` / `queued` / `running` / `completed` / `failed`7c 由 `converged` 改名,M9 迁移) (`INTEGER NOT NULL`):该时刻各状态计数。
### 2.8 `task_queue` (分布式任务队列表 - `mq`)
驱动分布式抢占与超时重试的核心表,使用 SQLite `WAL` 模式确保高吞吐并发安全(详见 [`sqlite_queue.rs`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/mq/src/sqlite_queue.rs#L65))。
- `task_id` (`VARCHAR(128) PRIMARY KEY`):任务 IDUUID)。
- `payload` (`TEXT NOT NULL`):序列化的 [`TaskSpec`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)(含 point_name / params / task_type / seed_point_name / workflow_name / tlusty_config / synspec_config 等)。
- `payload` (`TEXT NOT NULL`):序列化的 [`TaskSpec`](file:///home/fmq/program/tlusty/tl208-s54/dcts/crates/common/src/models.rs)
(含 point_name / params / seed_point_name / workflow_name / tlusty_config / synspec_config 等;
Phase 6 起无 `task_type` 字段,执行链由 `tlusty_config.strategies[0]` 派生)。
- `status` (`TEXT NOT NULL`)`pending`(就绪待领用)/ `claimed`(已被某 node 领用,计算中)。
- `created_at` (`DATETIME NOT NULL`):推入队列时间(同 `wave` 内 FIFO 排序键)。
- `claimed_at` (`DATETIME`):被领用的时间戳(`requeue_stale_tasks` 据此判定超时回投)。
+546
View File
@@ -0,0 +1,546 @@
# DCTS 数据库结构修复设计
> 本文档记录数据库结构的系统性修复方案(2026-08-05 立项)。
> 依据《docs/database.md》现状与调度/上报链路的实际数据流,覆盖迁移基础设施、
> tasks 阶段信息补全、索引、查询重写、死列清理与凭据留存决策、点级阶段化与命名语义修正。
>
> 关联文档:[database.md](./database.md)、[task_engine_decoupling_design.md](./task_engine_decoupling_design.md)、
> [dynamic_cpu_slots_design.md](./dynamic_cpu_slots_design.md)。
---
## 实施状态(2026-08-05 记录)
| Phase | 状态 | 关键改动 |
|---|---|---|
| 0 迁移基础设施 | ✅ 已实施 | `migrations.rs` + `PRAGMA user_version` 运行器,detect 守卫幂等 |
| 1 tasks 阶段信息 | ✅ 已实施 | M1 `failed_stage`/`summary_json`,结算落库 + 前端徽标 |
| 2 tasks 索引 | ✅ 已实施 | M2 `idx_tasks_wf_status_created` |
| 3 窗口函数化 | ✅ 已实施 | `ROW_NUMBER()` 重写逐点列表 + **§5.3 CTE 排序**(删 Rust sort_by |
| 4 死列清理 | ✅ 已实施 | M4 `DROP COLUMN revoked`registration_secret 留存 nodes |
| 6 删除 task_type | ✅ 已实施 | M6 `DROP COLUMN task_type`;执行链改 strategies[0] 推导 |
| 5a synspec 归因 | ✅ 已实施 | **M7**(版本号按部署序 >6`synspec_success_method` + 统计桶(含 WorkflowListStats |
| 5b 阶段状态列 | ✅ 已实施 | **M8** `tlusty_status`/`synspec_status`,结算/claim/running 同步,半失败守卫保留 tlusty 终态 |
| P9 归因拆分 | ✅ 已实施 | **M12** 新增 `tlusty_success_method` + 回填、**M13** `DROP COLUMN success_method`;整体归因改派生(前端 `tlusty ?? synspec` |
| 7a 注释+前端 | ✅ 已实施 | converged/max_relc 等注释契约;"已完成"标签;synspec 过滤器 |
| 7b 改名 | ✅ 已实施 | ChainStep/PhaseConfig/ResumePolicy/StepSummary、result_valid、trigger_tlusty_fallback、旧环境变量清理 |
| 7c converged→completed | ✅ 已实施 | **M9** 状态值迁移 + **M10** 快照列改名 + `GridPointStatus::Completed` + 前端/stats 键 |
| itek 全量保真 | ✅ 已实施 | `StepSummary.itek_history` 逐次迭代诊断随 summary_json 落库 |
| 文档同步 | ✅ 已实施 | `database.md` + `api.md` 同步新 schema/API |
> 每 Phase 合入前均通过全仓 `cargo test` + `cargo clippy -- -D warnings`。
### 审查发现与修复(2026-08-05subagent 对抗性审查)
| 严重度 | 发现 | 修复 |
|---|---|---|
| 【严重】CRITICAL | 7c 的测试断言 perl `"converged"(?!:)` 误把结算阶段值 `then_some("converged")` 改成 `"completed"`,而 claim/running 守卫仍查 `'converged'`**半失败重试时 tlusty_status 被覆盖,守卫完全失效**(147 测试未捕获,因测试断言了同样的错误值) | 结算阶段值回退 `'converged'`;修正测试断言;补**生命周期测试**(结算→claim→running→重试结算全程验证守卫保留 tlusty_status |
| 【高】HIGH | claim/running 守卫同时保留了 synspec 'failed',与设计 §7.3 打开项 #2「仅 synspec 侧流转」不符(重试期间 synspec_status 停在旧 failed | claim/running 改:tlusty 保留终态守卫 + **synspec 自由流转**为 queued/running |
| 【中】MEDIUM | `mark_grid_point_imported` 不设阶段列,导入点与正常点阶段口径不一致 | 导入置 `tlusty_status='converged'`synspec 保持 NULL |
| 【中】MEDIUM | 半失败分支隐含依赖 tlusty_enabled=true(脆弱的隐式不变量) | 加 `tlusty_enabled.then_some("converged")` 防御性守卫 |
| 【轻微】LOW | stats 内部 SQL 别名 `AS converged` 仍用旧命名 | 改名 `AS completed`(内部别名,不影响 JSON |
| 【轻微】LOW | M9 detect 数据检测在"空表"边界返回已迁移(跳过) | 接受为已知局限(FROM 保留 'converged' legacy 别名兜底) |
---
## 0. 背景与问题清单
对现有 schema`crates/server/src/db.rs` `init_tables` + `crates/mq/src/sqlite_queue.rs`)与
数据流(调度 → 队列 → 节点执行 → 上报结算)的审计,确认以下结构性问题:
| 编号 | 问题 | 严重度 | 现状 |
|---|---|---|---|
| P1 | `tasks` 表阶段结果不完整:`failed_stage``summary_json`synspec_rc/error/sec 全量)上报后即丢 | 高 | 结算 UPDATE 只落 7 个标量(db.rs:1943),阶段细节仅存磁盘 conv.json |
| P2 | `tasks` 表无 `workflow_name` 单列索引,`COUNT(*) WHERE workflow_name=?` 全表扫 | 中 | 仅有 `idx_tasks_point_wf_time(point_name, workflow_name, completed_at)` |
| P3 | 逐点列表 N+1 相关子查询(每 grid_point 行执行一次取最新任务) | 中 | db.rs:2780 / 2818 `LEFT JOIN ... (SELECT ... LIMIT 1)` |
| P4 | `registration_secret`(鉴权凭据)放在 `nodes`(运行时态表),与 `token_hash` 分属两表 | 中(已决策保留) | 审批前状态,`node_credentials.token_hash NOT NULL` 约束决定其只能存 nodes;P4 仅清 `revoked` 死列 |
| P5 | `node_credentials.revoked` 死列,新代码不读写 | 低 | db.rs:513 注释确认 |
| P6 | `grid_points.status` 单值掩盖两阶段管线:半失败点(大气收敛+光谱失败)点级只有 `failed`"大气就绪"不可查询 | 高(立项) | 阶段事实仅存于 tasks 历史 |
| P7 | 迁移机制脆弱:全部列变更在 `init_tables` 内手写 `PRAGMA table_info` + `ALTER`,无版本追踪 | 中 | 已积累 9+ 块幂等 ALTER |
| P8 | `task_type` 字段冗余:与 `tlusty_strategies[0]` 恒等,靠派发时同步维护不变量;synspec-only 场景为"假值" | 低 | 已废弃兼容字段,全量删除(Phase 6) |
| P9 | TLUSTY-first 命名残留:一批标识符保留单阶段语义,两阶段管线实现时存在语义误判风险(subagent 全库审计) | 中(立项) | `converged`/`stage`/`seed_*`/旧环境变量见 Phase 7**`success_method` 值域混用列已拆分(M12/M13** |
> **已确认不做**:`tasks` 表清理/保留策略(用户决策 2026-08-05,仅加索引)。
> **已确认不做**`grid_points.id` AUTOINCREMENT 移除(一切按 `(workflow_name, name)` 访问,表重建不值当)。
**基础约束**
- 双库分工不动:主库(状态/审计)与队列库(热路径抢占)保持分离。
- 新增列一律可空,旧节点上报不破坏结算。
- bundled SQLite 3.45+libsqlite3-sys 0.28)支持 `DROP COLUMN`、窗口函数、JSON1、`UPDATE...RETURNING`
---
## 1. 总体设计原则
1. **迁移先行**:所有结构变更走 Phase 0 建立的版本化迁移,不再新增 `init_tables` 手写 ALTER 块。
2. **可独立交付**:每 Phase 独立可上线、可回滚、带测试;全量 `cargo test` 通过才合入。
3. **兼容优先**`failed_stage` 缺省兜底 `"tlusty"`(与 api/task.rs:312 回退默认一致);新列全可空。
4. **观测不塞进热路径**:阶段细节落库在结算事务内完成,不为查询便利增加运行时开销。
---
## 2. Phase 0 — 版本化迁移基础设施
### 2.1 目标
消灭 `init_tables` 内继续堆积手写 ALTER 块(P7),为 P1–P6 提供统一的迁移通道。
### 2.2 设计
引入 `PRAGMA user_version` 驱动的迁移运行器。**V0 定义为 0**(`PRAGMA user_version` 对全新库的默认值),后续编号迁移从 1 开始:
```
Database::new
├─ init_tables() 现有幂等 bootstrap:建全表 + 既有列检测补全 → 置 user_version = V0(=0)
└─ apply_migrations() 从当前 user_version 顺序应用 M1..Mn → 每个迁移独立事务
```
- **新库**bootstrap 建表(CREATE TABLE 始终是最新形态,含后续 Phase 新增列)→ 置 `V0` → 应用 `V0+1..N`。**每个迁移自带 detect 守卫,已存在的列/索引直接跳过**,故新库上所有迁移为 no-op。
- **旧库**user_version=0 但表已存在):bootstrap 幂等跑一遍(含既有列检测补全)→ 置 `V0` → 应用后续迁移。detect 守卫保证只补缺的列/索引,数据零搬运。
- **幂等性关键**SQLite 无 `ADD COLUMN IF NOT EXISTS`,迁移必须靠 `detect` 守卫而非裸 SQL 数组实现幂等(审查 CRITICAL#3 修复——否则全新库上 bootstrap 已建新列,迁移再 ADD 会报 duplicate column 崩启动)。
- 迁移定义集中到新文件 `crates/server/src/migrations.rs`
```rust
pub struct Migration {
pub version: u32, // > V0(=0) 的顺序号(1..N
pub name: &'static str, // 便于日志与审计
pub detect: fn(&Connection) -> Result<bool>, // 该迁移是否已应用(列/索引存在性)
pub up: &[&str], // 未应用时才执行,同事务内顺序执行
}
pub const MIGRATIONS: &[Migration] = &[ /* M1, M2, M4, M6, M7(=5a) */ ];
> ** 2026-08-05** §11
> 5a Phase 6 **M7** M5 v6
> `version <= current` 5a`synspec_success_method`
pub fn current_version(conn: &Connection) -> u32; // PRAGMA user_version
pub fn apply_migrations(conn: &mut Connection) -> Result<()>;
```
- 运行器逻辑:读 `current_version` → 对每个 `version > current` 的迁移,先跑 `detect`
- `detect = true`(已应用,如新库 bootstrap 已建列)→ 仅推进 `user_version`,不执行 `up`
- `detect = false``BEGIN IMMEDIATE` → 执行 `up` SQL → `PRAGMA user_version = V``COMMIT`
- 中途失败不推进版本(进程启动时重试)。
- 与现有 `sqlite_queue.rs` 的迁移惯用法(PRAGMA table_info 检测)并存:队列库无用户版本迁移,维持现状;**仅主库**引入版本号。
### 2.3 代码改动
| 文件 | 改动 |
|---|---|
| `crates/server/src/migrations.rs` | 新增(迁移表 + 运行器) |
| `crates/server/src/db.rs` | `init_tables` 末尾置 `user_version = V0``Database::new` 在 bootstrap 后调用 `apply_migrations` |
### 2.4 测试
1. 新库全流程:`Database::new` → 版本 = 最新,全部迁移已应用。
2. 旧库升级:手工构造"缺若干列/索引"的旧 schema 库 → bootstrap + 迁移 → 断言列/索引/数据完整。
3. 迁移中断恢复:模拟 M2 中途失败 → user_version 停在 M1 → 重跑后 M2 成功。
---
## 3. Phase 1 — tasks 表阶段信息补全(P1)
### 3.1 目标
让"哪个阶段失败"`failed_stage`)与"完整阶段结果"`summary_json`)在 DB 可查,替代"只能翻磁盘 conv.json"。
### 3.2 数据流核实(关键前提)
节点上报 `TaskReport`models.rs:496-513)已携带全部所需字段:
- `converged` = **TLUSTY 大气收敛标志**reporter.rs:32 注释确认,非整管线);
- `status` = 整管线成败(`derive_report_status`:半失败 = Failedreporter.rs:38-45);
- `failed_stage` = `"tlusty"`/`"synspec"` 精确归因(`infer_failed_stage`reporter.rs:15-27);
- `summary_json` = Rust 侧 `ModelSummary` 序列化(含 `synspec_rc`/`synspec_error`/`synspec_sec` 与各子步骤摘要)。
服务端 `record_task_report`db.rs:1920)已持有 `&TaskReport`——**无需改签名**,只在结算 UPDATE 补两列。
> **保真边界(审查修正)**`ModelSummary`models.rs:599-616)与 `StageSummary`577-595**不含 conv.json 的逐次 itek 迭代数组**——该细节在 Rust serde 往返中结构性丢失,仍仅存磁盘 conv.json。落库的 summary_json 是"阶段级摘要"(含 last_iter / worst_depth / n_depths 等部分迭代诊断),非原始全量。若需完整逐次迭代入库,须给 `StageSummary` 加 `#[serde(flatten)]` 保留字段(暂缓,先文档化此边界)。
### 3.3 迁移 M1tasks 表,均在线 ADD COLUMN
```sql
ALTER TABLE tasks ADD COLUMN failed_stage TEXT; -- "tlusty" / "synspec"
ALTER TABLE tasks ADD COLUMN summary_json TEXT; -- 完整 ModelSummary 全保真
```
### 3.4 代码改动
| 位置 | 改动 |
|---|---|
| db.rs:1943 结算 UPDATE | `SET` 增加 `failed_stage = COALESCE(?10, 'tlusty')``summary_json = ?11`;绑定 `report.failed_stage` / `report.summary_json` |
| db.rs `list_point_attempts`2848 | SELECT 增加 `failed_stage``summary_json` |
| `common/src/models.rs` `AttemptRow` | 增加 `failed_stage: Option<String>``summary_json: Option<String>``#[serde(default)]` 保 API 兼容) |
| dashboard 逐尝试列表 | 阶段徽标(TLUSTY / SYNSPEC+ synspec 错误摘要(解析 summary_json |
> **错误路径**reporter.rs:85 失败上报的 `summary_json = {"error": ...}` 非 ModelSummary——前端解析必须容错(解析失败即显示错误文本),服务端仅透传不解析。
### 3.5 测试
- 全状态往返:completed / failed / timeout / 半失败(大气成+光谱败)/ synspec-only / 旧节点(failed_stage=None → 兜底 "tlusty")。
- `record_task_report` 后 tasks 行 `failed_stage``summary_json` 与上报一致。
- `list_point_attempts` 返回新字段。
- 错误路径:summary_json 为 `{"error":...}` 时前端容错展示。
---
## 4. Phase 2 — tasks 表索引(P2,仅索引,不做清理)
### 4.1 目标
消除 `workflow_name` 单列查询的全表扫。
### 4.2 迁移 M2
```sql
CREATE INDEX IF NOT EXISTS idx_tasks_wf_status_created
ON tasks(workflow_name, status, created_at);
```
> **用户决策(2026-08-05**:不做 `tasks` 清理/保留策略。`tasks` 作为完整审计日志长期保留;
> 此索引覆盖 `COUNT(*) WHERE workflow_name=?` 及 `find_stale_pending_points` 的 JOIN 侧(按 point_name+workflow_name 定位)。
> 注意:`find_stale_pending_points` 的过滤条件(`status='pending' AND created_at < ...`,无 workflow_name 前置)**不受此索引覆盖**——该查询由 JOIN 侧既有 `idx_tasks_point_wf_time` 支撑,随日志增长扫描量线性增加,属已接受的权衡。
### 4.3 测试
- `EXPLAIN QUERY PLAN` 验证 `SELECT COUNT(*) FROM tasks WHERE workflow_name=?` 走索引扫描。
- 既有 `find_stale_pending_points` / 工作流统计测试不回归。
---
## 5. Phase 3 — 逐点列表窗口函数化(P3)
### 5.1 目标
消除 `get_workflow_points` / `get_workflow_point_row` 的逐行相关子查询(数千点 → 数千次子查询)。
### 5.2 设计
用单遍 `ROW_NUMBER()` 窗口取每点最新任务,替代 `LEFT JOIN tasks ON task_id = (SELECT ... LIMIT 1)`
```sql
SELECT * FROM (
SELECT gp.name, gp.teff, ..., gp.status, gp.tlusty_success_method, gp.synspec_success_method, gp.attempt_count,
t.max_relc, t.task_type, t.seed_point_name, t.node_id,
t.completed_at, t.error_message, t.failed_stage, t.summary_json,
COALESCE(t.elapsed_sec, gp.last_elapsed_sec) AS eff_elapsed,
ROW_NUMBER() OVER (
PARTITION BY gp.name, gp.workflow_name
ORDER BY t.completed_at IS NULL, t.completed_at DESC, t.created_at DESC
) AS rn
FROM grid_points gp
LEFT JOIN tasks t
ON t.point_name = gp.name AND t.workflow_name = gp.workflow_name
) WHERE rn = 1 AND {where_sql} ORDER BY ... {limit} {offset}
```
外层再叠加筛选/排序/分页,保持与 `get_workflow_points` 现签名(`(total, points)` + 过滤 + 分页)一致。
`get_workflow_point_row`(单点)同样改写,外层 `WHERE workflow_name=? AND name=? AND rn=1`
### 5.3 附带优化(可选)
`claim_pending_grid_points`db.rs:1246SQL 与 Rust 侧双重排序(db.rs:1282 `list.sort_by`):
`UPDATE...RETURNING` 不保序,用 CTE 让子查询排序贯穿,删掉 Rust 侧 `sort_by`
### 5.4 测试
- 用既有测试网格样本断言新旧查询**结果集逐行等价**(含:无任务点 → LEFT JOIN 为 NULL、多点多尝试 → 取最新、`completed_at NULL` 排序规则)。
- **边界回归(审查修正)**:构造"点只有未完成任务(completed_at 全 NULL"的样本,断言 ROW_NUMBER 与相关子查询取到**同一行**——窗口函数与子查询的 NULL 排序语义须显式验证等价。
- 分页 `total` 计数不变。
---
## 6. Phase 4 — 死列清理(P5registration_secret 保留在 nodes
### 6.1 目标
清除死列 `node_credentials.revoked`P5)。**`registration_secret` 不迁移**——审查确认移动它存在两个结构性障碍(见 §6.2),且保留在 nodes 语义自洽。
> **决策**:保持两表分离不合并;`registration_secret` 保留在 `nodes`,加注释消除"凭据半吊子"的困惑。
### 6.2 为什么 registration_secret 不迁移(审查 CRITICAL#1/#2
1. **数据丢失**`register_node`db.rs:589)只 INSERT `nodes`,不建 node_credentials 行;`approve_node`db.rs:618)才建。pending 节点无 node_credentials 行,`UPDATE` 迁移会静默跳过它们,`DROP COLUMN` 后凭据永久丢失且无恢复路径。
2. **schema 约束**`token_hash TEXT NOT NULL`db.rs:518+ `idx_node_credentials_token_hash` 唯一索引——为 pending 节点 INSERT 空占位会撞唯一索引;改 nullable 需整表重建。
3. **语义**registration_secret 是**审批前**一次性凭据(注册时产生、取 token 前消费),node_credentials 是**审批后** token 状态——放 nodes 与 `status='pending_approval'` 同生命周期,自洽。
### 6.3 迁移 M4(清死列)
```sql
ALTER TABLE node_credentials DROP COLUMN revoked; -- bundled SQLite 3.45+,涉及表重建,低峰窗口
```
### 6.4 代码改动
- 仅注释:`nodes.registration_secret` 列加注释"审批前一次性凭据,唯一例外;token 哈希见 node_credentials"。
- `register_node` / `approve_node` / `check_status` / `reject_node` 全链路**零改动**。
### 6.5 测试
- `revoked` 列消失,`CREATE TABLE IF NOT EXISTS` 对新库不再创建该列。
- 注册 → 审批 → 取 token 全链路回归(registration_secret 行为不变)。
---
## 7. Phase 5 — grid_points 阶段化(立项)
### 7.1 目标
解除 P6:点级 `status` 单值无法表达两阶段管线。半失败点(大气收敛 + 光谱失败)需在点级可识别"大气就绪、光谱待重试";synspec 收敛归因需点级可查。
### 7.2 阶段语义(基于已核实的上报数据)
`record_task_report` 结算时可从报告精确推导各阶段状态:
| 上报条件 | 整体 status | tlusty_status | synspec_status |
|---|---|---|---|
| `tlusty_enabled=0` | — | NULL(不适用) | 由 `synspec_rc`/`synspec_error` 定:成功→converged,失败→failed |
| `converged=true` + synspec 成功 | converged | converged | converged |
| `converged=true` + synspec 失败(半失败) | failed | **converged** | failed |
| `converged=false`tlusty 启用) | failed | failed | pending(未运行) |
- `converged` = 大气收敛标志(reporter.rs:32);synspec 成败取自 `summary_json.synspec_rc` / `synspec_error`
- 整体 `status` 仍是调度/策略/筛选的**权威状态**,阶段列是补充可查信息,不替代它。
### 7.3 分步交付
#### 5a(先做,轻量):`synspec_success_method` 归因列
```sql
ALTER TABLE grid_points ADD COLUMN synspec_success_method TEXT;
```
结算时(settlement 事务内)设置,镜像既有大气归因的 synspec 分支逻辑(db.rs:1973-1984)。
**已实施(P9 拆分,M12/M13**:原 `success_method` 值域混用列拆为 `tlusty_success_method` + `synspec_success_method`
两个阶段列并删除原列,整体归因改由消费方派生(`tlusty ?? synspec`)。
```sql
CASE WHEN synspec
THEN json_extract(tasks.synspec_strategies, '$[0]')
END
```
收益:解锁"光谱以 standard 等策略收敛了多少点"的 SQL 统计。无瞬态状态同步负担。
**统计 SQL 改动归本阶段独占**db.rs:2325-2574 加 synspec 策略桶),P7a 只做前端过滤器引用,避免两阶段重复改同一处(审查 HIGH#2)。
#### 5b(再议,完整):阶段状态列
```sql
ALTER TABLE grid_points ADD COLUMN tlusty_status TEXT; -- pending/queued/running/converged/failed
ALTER TABLE grid_points ADD COLUMN synspec_status TEXT; -- pending/running/converged/failed/not_required
```
**同步触点**(与 `grid_points.status` 的既有更新点对齐,均为机械追加列):
- `claim_pending_grid_points`db.rs:1246)→ 相关阶段列置 `queued`
- `mark_grid_point_running`db.rs:1508)→ 相关阶段列置 `running`
- `record_task_report` 结算(db.rs:1994-2004)→ 按 7.2 语义表置阶段终态;
- 回退重置(scheduler.rs `trigger_strategy_fallback`)→ 半失败重试时保持 `tlusty_status=converged`,仅重置 `synspec_status`
**打开项(5b 实施前需定)**
1. `not_required`(阶段关闭)与 `NULL`(未配置)的取值约定;
2. 半失败重试期间 `tlusty_status` 是否可被后续 claim 覆盖(建议:不可,仅 synspec 侧流转);
3. `initialize_grid` 各策略(skip_converged / skip_failed / force_recompute)如何对待阶段状态(建议:以整体 `status` 为准,阶段列只读展示)。
> 建议 5b 在 5a 上线、`summary_json` 数据积累后评估——届时"阶段状态是否需要独立列,还是 join tasks 已够"有真实数据支撑。
---
## 8. Phase 6 — 删除 task_type(全量删除,P8
### 8.1 目标与前提
消除冗余字段、synspec-only 假值坑、以及"派发时同步维护 `task_type == strategies[0]` 不变量"的负担。
**前提(用户决策 2026-08-05**:集群**无历史节点**,server/node 随同一镜像统一重建,不存在旧节点反序列化兼容问题。
### 8.2 执行链改由 strategies 推导(runner + executor
现状:executor 传 `task.task_type``custom_chain` 恒为 `None`executor.rs:172-175),runner 用 task_type 选执行链(runner.rs:299-301)。
改造:executor 按 `tlusty_config.strategies[0]` 显式推导执行链并作为 `custom_chain` 传入:
```rust
// executor.rs
let chain = match task.tlusty_config.current_strategy("cold_run") {
"seed_step" => common::runner::default_seed_chain(),
_ => common::runner::default_cold_chain(),
};
runner.run_model_with_timeout(..., Some(chain), ...);
```
runner`run_model_with_timeout``task_type: TaskType` 参数改为 `current_strategy: &str`
`unwrap_or_else` 兜底改按策略串匹配;`TaskType` 枚举整体删除。
`default_cold_chain()` / `default_seed_chain()`runner.rs:16, 66)已 `pub`,无需改动。
### 8.3 models.rs
- `TaskSpec` 移除 `task_type` 字段(models.rs:398);
- 删除 `normalize_compat` 的 task_type 回填分支(models.rs:435-444);
- 删除 `TaskType` 枚举定义。
### 8.4 调度器(scheduler.rs
- 删除 `strategy_to_task_type`239-244);
- 全部 `TaskSpec { task_type: ... }` 构造(465, 776, 836)移除该字段。
### 8.5 DB 迁移 M6 + 配套查询改造
```sql
ALTER TABLE tasks DROP COLUMN task_type; -- bundled SQLite 3.45+,涉及表重建,低峰窗口
```
| 位置 | 改造 |
|---|---|
| db.rs:1979 `success_method` 归因 | `ELSE task_type``ELSE json_extract(tlusty_strategies, '$[0]')`(不变量成立时等价,从此归因全派生)。**后经 P9 拆分(M12/M13)改 `tlusty_success_method` + `synspec_success_method` 两阶段列并删原列** |
| db.rs:1777-1780 `insert_task` | 删除 `task_type_str` 解析与绑定 |
| db.rs:2848 `list_point_attempts` | SELECT 去 `task_type` |
| db.rs:1859-1866 `has_pending_tasks_for_point` | `Some(tt)` 分支 `AND task_type = ?3``AND json_extract(tlusty_strategies, '$[0]') = ?3`(生产仅传 `None`,此分支测试用) |
| models.rs `AttemptRow` / API 响应 | 去 `task_type` 字段(`#[serde(default)]` 兼容已发前端) |
### 8.6 前端
- `pointsTable.js:222``last_task_type`
- `pointPanel.js:116``task_type` 徽标;
- `overview.js:211` 注释清理。
### 8.7 测试
- 全量修正 `TaskType::ColdRun` / `TaskType::SeedStep` 引用(实际计数:scheduler.rs 18 处、db.rs 14 处 + AttemptRow 字段、api_tests.rs 20+ 处 JSON 断言、sqlite_queue.rs 7 处、runner.rs 5 处、executor.rs 2 处——编译器兜住全部,属纯工作量);
- 新增:executor 按 `strategies[0]="seed_step"` 走种子链的用例;
- `has_pending_tasks_for_point` 过滤改 `strategies[0]` 后的等价断言。
### 8.8 升级注意
队列库(`dcts_queue.db`)可能残留升级前序列化含 `task_type` 的在途 payload。新代码 serde 默认忽略未知字段可正常反序列化,但**旧 `seed_step` 消息的 `strategies` 可能被 serde default 填成完整链 `[cold_run, seed_step]` 导致误判 cold_run**——升级前确认队列为空(或停机时清空队列)。
### 8.9 收益与风险
**收益**:消除冗余列与假值坑;不再维护同步不变量;归因/过滤全部改派生口径。
**风险**:runner 执行链行为需回归(seed_step 消息必须走种子链);测试改动面大但机械。
---
## 9. Phase 7 — 命名语义修正(TLUSTY-first 残留,P9
### 9.1 背景
系统最早仅 TLUSTY(SYNSPEC 默认不可配置),演进为两阶段管线后,一批标识符保留"仅 TLUSTY"语义,
实现完整计算过程时存在语义误判风险。本 Phase 由 subagent 全库审计产出(2026-08-05),
分注释契约、前端修正、改名、清理四档。
### 9.2 最危险的 5 个语义混淆
| # | 名称 | 位置 | 问题 | 处置 |
|---|---|---|---|---|
| 1 | `converged` | models.rs:603 / reporter.rs:32 / runner.rs:605-616 | TLUSTY 启用时=大气收敛;synspec-only 被重写为管线成功;读方无法凭字段名判断语义 | 7a 注释契约 + 7b 改名 |
| 2 | `StageConfig` vs `EngineStageConfig` | runner.rs / models.rs:330 | "stage"同词两层义:TLUSTY 计算链子步骤(lte/nc/nl) vs TLUSTY/SYNSPEC 管线大阶段;易把 SYNSPEC 误加进 `stages` | 7b 改名 |
| 3 | `success_method` 值域 | db.rs:1973-1984 | synspec-only 塞入策略名(standard);统计分桶(db.rs:2325-2574)与前端过滤器仅认 cold_run/seed_stepsynspec 收敛点统计丢失 | **已解决(M12/M13 拆分 + 5a 加桶)**:拆 `tlusty_success_method`/`synspec_success_method` 两阶段列,synspec-only 不再污染大气列 |
| 4 | `seed_point_name` / `seed_atmos_path` | models.rs:399 / executor.rs:70-83 | TLUSTY seed_step=热启动种子;synspec-only=光谱输入大气来源点,双义无注释 | 7a 注释 |
| 5 | `task_type` 余光 | scheduler.rs:458 | synspec-only 固定填 cold_run,前端/CSV 误标"冷启动" | Phase 6 已覆盖;7a 处理前端残留 |
### 9.3 分步实施
**7a(先做,无破坏性)——注释契约 + 前端可感知修正**
- **注释契约**(不改名):
- `converged` 双义标注(`TaskReport.converged` = "本次结果是否可用",非"大气收敛");
- `max_relc` / `final_max_relc` 标注"仅 TLUSTY 大气迭代有效"
- `cno_sum` / `wave` 标注"TLUSTY 大气金属丰度排序"
- `elapsed_sec` vs `synspec_sec` 标注包含关系(总耗时含 TLUSTY+SYNSPECsynspec_sec 是子集);
- `seed_point_name` 标注 synspec-only 双义。
- **前端可感知修正**(bug 级):
- "已收敛" → "已完成"pointsTable.js:44 等所有中文标签);
- "方法"过滤器增加 synspec 策略名选项(pointsTable.js:48-51);
- CSV 列 `last_task_type``overall_method`pointsTable.js:218P9 拆分后改 `overallMethod(p) = tlusty ?? synspec` 派生值);
- `pointMethodBadge(a.task_type)` 改从 strategy 推导(pointPanel.js:116);
- "阶段链诊断" → "TLUSTY 收敛链诊断"pointPanel.js:152);
- "方法"过滤器的 synspec 选项数据源 = Phase 5a 落库的 `synspec_success_method`(统计 SQL 改动归 5a 独占,本阶段不再触碰 db.rs:2325-2574)。
**7b(Phase 6 后,纯改名,无行为变化)**
- `StageConfig``ChainStep``StageSummary``StepSummary`TLUSTY 子步骤层);
- `EngineStageConfig``PhaseConfig``StagePolicy``ResumePolicy`(管线大阶段层);
- `ModelSummary.stages` 加注释"TLUSTY iteration steps, NOT pipeline phases"
- `TaskReport.converged``result_valid`
- `trigger_seed_step_fallback` 删除(或改 `trigger_tlusty_fallback`);
- 旧环境变量回退清理:`CNO_PORT` / `CNO_SERVER_URL` / `DCTS_RESULTS_DIR` / `DCTS_ARCHIVE_DIR``GridConfig.results` 死字段。
**7c(可选,大迁移;**2026-08-05 已实施****
- `GridPointStatus::Converged``Completed`enum 改名 + DB 文本值 `'converged'``'completed'`(全链 SQL
字面量 + 数据迁移 **M9** + 快照列改名 **M10** + 前端/统计键)。`From<&str>` 保留 `'converged'`/`'done'`
为 legacy 别名(M9 前旧数据)。实施细节:M9 `UPDATE grid_points SET status='completed' WHERE status='converged'`
M10 `RENAME COLUMN workflow_progress_snapshots.converged → completed`stats 顶层键 `"converged"``"completed"`
`cold_run_converged`/`seed_step_converged`/`synspec_converged` 方法限定键保留)。
### 9.4 测试
- 7a:前端过滤/统计对 synspec 收敛点的等价断言;注释类无测试。
- 7b:改名后全量 `cargo test`(编译期即验证)+ `cargo clippy -- -D warnings`
- 7c:M9/M10 迁移专项测试(状态值迁移、快照列改名)+ 全量回归。
### 9.5 数据库列名审计结论
DB 列名绝大多数**如实反映存储内容**,无需改名;TLUSTY-first 残留集中在**列值**与**概念**层面而非列名。
逐表核实结论如下。
**需要改动的列(已由既有 Phase 覆盖)**
| 列 | 动作 | 对应 |
|---|---|---|
| `tasks.task_type` | 删除 | Phase 6 (M6) |
| `grid_points.success_method` | **已删除**:值域混用列拆为 `tlusty_success_method` + `synspec_success_method`M12/M13);整体归因派生 `tlusty ?? synspec` | P9 拆分 |
| `tasks.max_relc` | 加注释"仅 TLUSTY 大气迭代有效";**不改名**(改名需动全链 SQL,收益低) | Phase 7a |
**列名级残留(仅一处 + 一个耦合项)**
1. `workflow_progress_snapshots.converged` 列名:已随 7c 实施改名为 `completed`(M10)——存"管线完成点数"。
2. `grid_points.status``'converged'` 值:列名 `status` 无问题,**值**是 TLUSTY-first(暗示大气收敛,实为管线完成)。7c 已实施:值改为 `'completed'`(M9 数据迁移 + 全链 SQL 字面量 + 前端)。
**核实后无需改动的列**
- `tasks.seed_point_name`:DB 列只存 TLUSTY 热启动种子,synspec-only 派发时恒 `None`scheduler.rs:453);"双义"发生在 executor 局部变量 `seed_atmos_path`,不在 DB 列上,7a 注释即可。
- `grid_points.cno_sum` / `wave`:存的就是 TLUSTY 丰度排序量,名实相符,7a 加物理语义注释。
- `tasks.elapsed_sec` / `attempt_count` / `atmosphere_ref` / `tlusty_*` / `synspec_*` 家族:全部准确。
**判定原则**:DB 列改名成本高(迁移 + 全链 SQL + 前端 + 历史数据),仅在"列名本身误导"时才值得。本审计确认除 `snapshots.converged`(耦合项)外**无列名级误导**,其余均为列值/概念问题,走注释与派生口径解决。
---
## 10. 测试策略
沿用仓库现有惯例(db.rs / scheduler.rs 内 `#[tokio::test]` 模块 + `tests/` 集成):
1. **迁移测试**:新库全流程;旧 schema 库升级;迁移中断恢复(Phase 0)。
2. **Phase 1**`record_task_report` 全状态往返(含半失败、synspec-only、旧节点兜底)。
3. **Phase 2**:索引生效(EXPLAIN)+ 既有统计测试不回归。
4. **Phase 3**:新旧查询结果集逐行等价 + 分页计数不变。
5. **Phase 4**:新库无 `revoked` 列;注册 → 审批 → 取 token 全链路(registration_secret 行为不变)。
6. **Phase 5**:半失败点结算后 `tlusty_status=converged``synspec_status=failed`;重试期间不覆盖 `tlusty_status``synspec_success_method` 归因正确。
7. **Phase 7**7a 前端过滤/统计对 synspec 收敛点等价断言;7b 改名后编译期全量通过。
每 Phase 合入前:全量 `cargo test` + `cargo clippy -- -D warnings`
---
## 11. 部署与回滚
- **迁移安全性**P1/P2/5a 的 `ADD COLUMN` / `CREATE INDEX` 全在线,无停写窗口;
Phase 4M4)与 Phase 6M6)的 `DROP COLUMN` 涉及表重建,**部署前做手动备份**(已有每日 `backup_database` + 7 天保留兜底),在低峰窗口执行。
- **回滚**SQLite `user_version` 迁移无 down 迁移;回滚 = 从备份恢复 + 重放后续版本。`ADD COLUMN` 类变更对旧二进制无害(新列新代码写、旧代码不读)。
- **部署顺序**Phase 0 → 1 → 2 → 3 → 4 → 6 →(5a)→(5b)→(7a)→(7b),每阶段独立发版。
- Phase 6 前确认队列为空(见 §8.8);
- Phase 7a 无 schema 依赖,可与 Phase 1–4 并行实施;其统计/过滤修正与 5a 合并;
- Phase 7b 改名在 Phase 6 之后(避免与 task_type 删除的改动冲突)。
---
## 12. 决策记录
| 决策 | 结论 | 依据 |
|---|---|---|
| tasks 清理/保留策略 | **不做**(仅加索引) | 用户决策 2026-08-05:tasks 作为完整审计日志长期保留 |
| nodes / node_credentials | **不合并**registration_secret 保留在 nodes(审批前状态),仅清 `revoked` 死列 | 写频差异显著;token_hash NOT NULL + 唯一索引决定 secret 只能存 nodes(审查 CRITICAL#1/#2 |
| `node_credentials.revoked` | 删除 | 死列,新代码不读写 |
| `grid_points.id` AUTOINCREMENT | 保留 | 一切按 `(workflow_name, name)` 访问,表重建不值当 |
| Phase 5 | **立项**,先 5a 后 5b | 用户决策 2026-08-055b 待 summary_json 数据积累后评估 |
| task_type 字段 | **全量删除**Phase 6 | 用户决策 2026-08-05;集群无历史节点,随镜像统一重建,无旧节点兼容负担 |
| Phase 7 命名语义修正 | **立项**,先 7a(注释+前端)后 7b(改名),7c 已实施(2026-08-05 | 用户决策 2026-08-05subagent 全库审计确认 TLUSTY-first 命名残留 |
+20 -19
View File
@@ -24,7 +24,7 @@
> - **未实现**:§2.2/§2.4 的「重试失败点」按钮与 `POST .../points/retry` 端点
> (前后端均未实现)。
> - **方法归因**`imported` 不再作为独立收敛途径(导入点按 seed_step 统计),
> `success_method` 过滤白名单 `cold_run` / `seed_step`。
> 大气方法过滤白名单 `cold_run` / `seed_step`(映射 `tlusty_success_method`);另有 `synspec_only` 光谱专用点过滤器(`tlusty IS NULL AND synspec IS NOT NULL`
---
@@ -35,12 +35,12 @@
| 数据 | 位置 | 说明 |
|---|---|---|
| 网格点 6 维参数、`cno_sum``wave` | `grid_points` 表(`db.rs:246-263` | 按 `workflow_name` 分区,复合唯一 `(workflow_name, name)` |
| 点状态 `pending/queued/running/converged/failed` | `grid_points.status` | `GridPointStatus``models.rs:235-243` |
| **成功手段** `cold_run` / `seed_step` / `imported` | `grid_points.success_method` | 收敛时由成功任务的 `task_type` 回填(`db.rs:1097-1101`);失败点为 NULL |
| 点状态 `pending/queued/running/completed/failed``completed` 由 7c 改名自 `converged` | `grid_points.status` | `GridPointStatus``models.rs:235-243` |
| **大气成功手段** `cold_run` / `seed_step` / `imported` | `grid_points.tlusty_success_method` | TLUSTY 阶段收敛时由成功任务的 `tlusty_strategies[0]` 派生回填(Phase 6 起 `task_type` 列已删除);TLUSTY 禁用(synspec-only)为 NULL。光谱阶段另存 `synspec_success_method`Phase 5a,取 `synspec_strategies[0]`)。整体归因由消费方派生 `tlusty ?? synspec`P9 拆分,原 `success_method` 值域混用列已删) |
| 重试次数 | `grid_points.attempt_count` | 每次回报 +1`db.rs:1089-1095` |
| 每次尝试的收敛指标 | `tasks.max_relc` | 收敛判据 `max_relc < chmax`(默认 0.001`conv_check.rs:105-109` |
| **每次尝试的种子来源** | `tasks.seed_point_name` | 派发时写入(`scheduler.rs:194-205` |
| 每次尝试的方法 / 错误 / 节点 / 完成时间 | `tasks.task_type / error_message / node_id / completed_at` | 一个点多行(每次尝试一行),工作流删除前长期保留 |
| 每次尝试的方法 / 错误 / 节点 / 完成时间 | `tasks.tlusty_strategies[0] / error_message / node_id / completed_at` | 一个点多行(每次尝试一行),工作流删除前长期保留;方法取自策略链首项(`task_type` 列已删) |
| 全局种子库 | `seeds` 表 + `data/seeds/<name>/<name>.7` | 仅收敛且无 NaN 的点入库(`task.rs:187-207` |
| **逐阶段完整诊断**lte→nc→nl / seed_nc→nl | `data/seeds/<name>/conv.json` | `ModelSummary``models.rs:376-391`):每阶段 `converged / best_max_relc / chmax / elapsed_sec`、总耗时、`synspec_sec`、所用种子。**成功与失败的点都会写**(`task.rs:178-184` |
| 按工作流的统计聚合 | `db.get_grid_summary_stats(Some(wf))` | **已实现且有单测**`db.rs:1582-1639`),但没有任何 HTTP 端点调用它 |
@@ -67,7 +67,7 @@
所以"哪些参数冷启动能成功" ≈ 低 cno_sum / 中低温区的点;高温 He-poor 区几乎全靠种子步进。
- **还有一次性失败回退**:冷启动发散且存在邻居种子时,自动以 `seed_step` 重投一次
`scheduler.rs:270-350`,受 `seed_step_fallback` 配置与 `has_seed_step_attempt` 一次性闸门约束)。
因此 `success_method='seed_step'` 的点可能是"冷启动失败后被救回"的——该归因需从 `tasks` 多行历史还原。
因此 `tlusty_success_method='seed_step'` 的点可能是"冷启动失败后被救回"的——该归因需从 `tasks` 多行历史还原。
- **第三类 `imported`**:历史 `run_grid.py` 结果导入(`db.rs:966-981`),UI 需单独呈现为"历史导入"。
- **收敛判据**:末次迭代最差深度点的最大相对修正 `max_relc < chmax`(默认 1e-3),
且大气 NaN 占比 ≤ 10%`conv_check.rs:127-149`)。
@@ -215,9 +215,9 @@
"data": {
"name": "sdB_cno", "status": "running",
"total": 432,
"pending": 120, "queued": 40, "running": 8, "converged": 261, "failed": 3,
"cold_run_converged": 220, "seed_step_converged": 41, "imported_converged": 0,
"waves": [ {"wave": 0, "total": 108, "converged": 108, "failed": 0}, ... ],
"pending": 120, "queued": 40, "running": 8, "completed": 261, "failed": 3,
"cold_run_converged": 220, "seed_step_converged": 41,
"waves": [ {"wave": 0, "total": 108, "completed": 108, "failed": 0}, ... ],
"avg_point_sec": 740.0, "eta_sec": 9600
}
}
@@ -242,11 +242,11 @@
"name": "t60000_g5.0_he-2_c-4_n-4_o-4",
"teff": 60000, "logg": 5.0, "loghe": -2, "logc": -4, "logn": -4, "logo": -4,
"cno_sum": -12, "wave": 0,
"status": "converged", "success_method": "seed_step", "attempt_count": 2,
"last_max_relc": 0.00043, "last_task_type": "seed_step",
"status": "completed", "tlusty_success_method": "seed_step", "synspec_success_method": "standard", "attempt_count": 2,
"last_max_relc": 0.00043,
"seed_point_name": "t60000_g5.0_he2_c-4_n-4_o-4",
"node_id": "node-a1b2", "last_completed_at": "2026-07-30T11:12:00Z",
"elapsed_sec": 126.4
"last_elapsed_sec": 126.4
}
]
}
@@ -256,7 +256,7 @@
- "最近一次尝试"用关联子查询取 `tasks` 最新行:
```sql
SELECT gp.*, t.max_relc AS last_max_relc, t.task_type AS last_task_type,
SELECT gp.*, t.max_relc AS last_max_relc,
t.seed_point_name, t.node_id, t.completed_at AS last_completed_at,
t.error_message AS last_error
FROM grid_points gp
@@ -283,12 +283,13 @@ LIMIT ? LIMIT OFFSET ?;
"data": {
"point": { /* */ },
"attempts": [
{"task_id": "...", "task_type": "cold_run", "seed_point_name": null,
{"task_id": "...", "seed_point_name": null, // Phase 6 起无 task_type 字段
"status": "failed", "max_relc": 954000.0, "atmosphere_has_nan": true,
"node_id": "node-a1b2", "error_message": "nl stage diverged...",
"created_at": "...", "completed_at": "..."},
{"task_id": "...", "task_type": "seed_step", "seed_point_name": "t60000_...",
"status": "completed", "max_relc": 0.00043, ...}
"failed_stage": "tlusty", "summary_json": "{...}",
"created_at": "...", "completed_at": "...", "elapsed_sec": 1800.0},
{"task_id": "...", "seed_point_name": "t60000_...", // 第二次尝试(seed_step 热启动救回)
"status": "completed", "max_relc": 0.00043, "failed_stage": null, ...}
],
"conv": { /* data/seeds/<name>/conv.json ModelSummary null */ }
}
@@ -304,7 +305,7 @@ LIMIT ? LIMIT OFFSET ?;
请求体:`{"names": ["t80000_..."], "all_failed": true}`(二选一)。
- 前置:工作流必须 `running`(否则 400,提示"请先启动工作流"——避免重置后无调度器消费的僵尸态)。
- 逻辑:新 db 函数 `reset_failed_points(wf, names|all)`
`UPDATE grid_points SET status='pending', success_method=NULL WHERE workflow_name=? AND status='failed' [AND name IN (...)]`
`UPDATE grid_points SET status='pending', tlusty_success_method=NULL, synspec_success_method=NULL WHERE workflow_name=? AND status='failed' [AND name IN (...)]`
随后立即触发一次 `schedule_pending_tasks()`(不必等 30s tick)。
- 重试后的方法仍由调度器按种子可用性自动决定(大概率 seed_step,因为此时种子池已更丰富)——
这与领域语义一致,前端文案如实说明:"重试的点将由调度器自动选择冷启动或种子步进"。
@@ -316,7 +317,7 @@ LIMIT ? LIMIT OFFSET ?;
```json
{"name": "...", "status": "...", "description": "...",
"created_at": "...", "updated_at": "...",
"stats": {"total": 432, "converged": 261, "failed": 3, "running": 8,
"stats": {"total": 432, "completed": 261, "failed": 3, "running": 8,
"cold_run_converged": 220, "seed_step_converged": 41}}
```
@@ -332,7 +333,7 @@ LIMIT ? LIMIT OFFSET ?;
2. `StageSummary` 携带 `last_iter / worst_depth / n_depths``models.rs:364-373` 扩字段,
`runner.rs:302-317``ConvCheckResult` 已有值,只是没搬过去)→ conv.json 与点详情获得迭代数。
3. 新表 `workflow_progress_snapshots(workflow_name TEXT, ts DATETIME, pending INT, queued INT,
running INT, converged INT, failed INT)`:由 `main.rs:111-197` 的 30s 后台循环顺手写一行;
running INT, completed INT, failed INT)`:由 `main.rs:111-197` 的 30s 后台循环顺手写一行;
保留策略:仅当计数相对上次快照有变化才写;定期清理 24h 前的行。供"进度-时间"曲线与精确 ETA。
### 3.3 明确不做 / 保持现状