@quan-huayan/dsh-research-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/cordis.patch.yml +32 -0
- package/package.json +59 -0
- package/presets/research-engine/agent.cordis.yml +305 -0
- package/presets/research-engine/contract/conventions.schema.json +41 -0
- package/presets/research-engine/contract/fields.md +150 -0
- package/presets/research-engine/contract/managed.json +36 -0
- package/presets/research-engine/contract/manifest.schema.json +33 -0
- package/presets/research-engine/contract/note.schema.json +56 -0
- package/presets/research-engine/contract/observations.schema.json +46 -0
- package/presets/research-engine/contract/pipeline.schema.json +43 -0
- package/presets/research-engine/contract/project.schema.json +66 -0
- package/presets/research-engine/contract/templates.schema.json +78 -0
- package/presets/research-engine/plugins/engine-git/main.js +284 -0
- package/presets/research-engine/plugins/exp-ledger/main.js +1324 -0
- package/presets/research-engine/plugins/kb-core/main.js +538 -0
- package/presets/research-engine/plugins/stage-ctrl/main.js +388 -0
- package/presets/research-engine/plugins/task-dispatch/main.js +848 -0
- package/presets/research-engine/preset.yml +3 -0
- package/presets/research-engine/skills/research-engine/SKILL.md +114 -0
- package/scripts/install.mjs +116 -0
- package/startup.js +24 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: research-engine
|
|
3
|
+
description: 研究工程的工作方式:先看现状再动手,所有变更走工具,结论必须指向证据,用户权威必须来自真实答复。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 研究引擎:怎么在工程里工作
|
|
7
|
+
|
|
8
|
+
根本原则:**任何研究结论,都必须能由不可变证据 + 一条可重放的过程重新得到。**
|
|
9
|
+
|
|
10
|
+
三条推论决定你的每一个动作:
|
|
11
|
+
|
|
12
|
+
1. **证据优先** —— 结论只能指向证据;数字只能由工具从已登记文件里提取,不许手写。
|
|
13
|
+
2. **封闭变更** —— 工程里的所有变更只能经工具发生。用 write / edit / pwsh 直接改受管文件会被体检点名,并在你下次调用写类工具时被拒绝。
|
|
14
|
+
3. **权威分离** —— 用户权威、证据权威、agent 提议三者不得互相冒充。
|
|
15
|
+
|
|
16
|
+
## 一、工程的物理形态
|
|
17
|
+
|
|
18
|
+
| 位置 | 是什么 | 谁能写 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `project.yaml` | 环境:身份、受管区、遗留登记、生效约定 | `project_init` / `convention_declare` / `project_reconcile` |
|
|
21
|
+
| `pipeline.yaml` | 环境:阶段机(阶段 + 边 + 入口 + 图版本) | `stage_declare` |
|
|
22
|
+
| `templates.yaml` | 环境:可执行声明(命令白名单 + 参数契约 + 可提取字段) | `template_declare` |
|
|
23
|
+
| `scripts/` | 环境:辅助脚本与其元数据 | `script_declare` |
|
|
24
|
+
| `registry.jsonl` | 状态与事件台账(追加式) | 各工具内部 |
|
|
25
|
+
| `experiments/<runId>/` | 证据:`config.json`(实例冻结)、`manifest.json`(产出清单)、`observations.json`(提取值)、`raw/`(原始产出) | 工具 + 进程 |
|
|
26
|
+
| `.kb/notes/*.md` | 提议(`proposed`)与结论(`accepted`) | `note_write` / `note_adjudicate` |
|
|
27
|
+
| `_report/*`、`.kb/index.yaml`、`skills/<工程>/SKILL.md` | 派生视图(可重建,别手改) | `view_render` |
|
|
28
|
+
| `.research/state.json` | 派生状态(当前阶段) | `stage_goto` |
|
|
29
|
+
| 遗留区(`.pth/.png/.txt/.ipynb`、`data/`、`history/`、`x*/`) | 用户既有文件:只登记,不改不删 | 无人 |
|
|
30
|
+
|
|
31
|
+
## 二、按意图找工具
|
|
32
|
+
|
|
33
|
+
**看现状(永远先做这步)**
|
|
34
|
+
- `project_load` —— 工程全貌:身份、阶段机、模板、约定、最近体检
|
|
35
|
+
- `project_verify` —— 体检:受管区是否被外部改动、证据是否完整、结构是否合规
|
|
36
|
+
- `stage_read` —— 阶段机与当前节点、合法去向
|
|
37
|
+
- `stage_goto` —— `to=<阶段>` 沿已声明的边推进;`replay=true` 按台账重放物化派生状态(若重放会改变文件当前断言的值,即两个来源矛盾,必须由用户裁决)
|
|
38
|
+
- `run_query` —— 运行台账(状态、参数、产出数、观察值)
|
|
39
|
+
- `note_query` —— 知识:默认只列生效结论,提议单列
|
|
40
|
+
- `entity_query` —— 实体、已登记的用户裁决(含问答原文);`record=ask` 列出本会话的提问标识与答复
|
|
41
|
+
|
|
42
|
+
**接新工程**
|
|
43
|
+
- `project_init` —— 接入目录:发现现状、建受管白名单、登记遗留文件、初始体检
|
|
44
|
+
|
|
45
|
+
**声明环境(顺序有依赖)**
|
|
46
|
+
- `stage_declare` —— `action=stage` 声明阶段 → `action=edge` 连边 → 传 `entry` 设入口
|
|
47
|
+
- `script_declare` —— `mode=create` 新建脚本;`mode=adopt` 收编磁盘上已有脚本
|
|
48
|
+
- `template_declare` —— 引用已声明脚本,给出 `allow`、`paramsSchema`、**`observables`**(可提取字段,必须显式给,可以是 `[]`)
|
|
49
|
+
- `convention_declare` —— 把用户裁决写成生效约定(见第四节)
|
|
50
|
+
- `entity_declare` —— 登记研究对象(必须带可解析的 `source`)
|
|
51
|
+
|
|
52
|
+
**做实验**
|
|
53
|
+
1. `run_draft` —— 冻结实例:模板版本、参数、完整命令、脚本指纹、阶段、图版本 → 得到 `runId`
|
|
54
|
+
2. `run_launch` —— 安全闸 + 后台派发(不阻塞本轮);结束后自动登记 `raw/` 产出,并把进程输出收进 `raw/stdout.log`
|
|
55
|
+
3. `job_output` —— 运行中读日志;任务结束后完整输出在 `experiments/<runId>/raw/stdout.log`(已收进证据,`job_output` 此时可能为空);`job_kill` —— 取消
|
|
56
|
+
4. `run_observe` —— 从**已登记**产出里提取数值,形成证据权威;可 `compareTo` 另一个 run 做对比
|
|
57
|
+
5. `run_close` —— 作废(`invalidated`)或归档(`archived`,需用户裁决);**产出永不删除**
|
|
58
|
+
|
|
59
|
+
**沉淀知识**
|
|
60
|
+
- `note_write` —— `kind=claim` 必须带 `evidence[]` 与 `scope`;与已生效结论同范围冲突时只能写 `proposal`
|
|
61
|
+
- `note_adjudicate` —— `accept` / `retract` / `supersede`;有可解析证据的 claim 可依证据生效,其余需用户裁决
|
|
62
|
+
- `view_render` —— `kind=notes-skill` 生成工程笔记;`report` 生成运行报表;`index` 生成笔记索引
|
|
63
|
+
|
|
64
|
+
**修受管区**
|
|
65
|
+
- `project_reconcile` —— `mode=restore`(默认,取回已登记版本)/ `adopt`(接受外部改动,需用户裁决)/ `ignore`(登记为有意忽略)
|
|
66
|
+
|
|
67
|
+
## 三、闸门:不满足就是拒绝,不是提醒
|
|
68
|
+
|
|
69
|
+
- 写类工具前置:它要触及的受管文件必须与已登记版本一致。被外部改动 → 拒绝,先去 `project_reconcile`。
|
|
70
|
+
- `run_draft` / `run_launch` 另需:整个工程的受管区都没有未修复的改动;阶段已声明。
|
|
71
|
+
- `run_launch` 安全闸:命令前缀必须在模板 `allow` 内,脚本必须落在工程内。命中即拒绝,并记入教训库,run 保持 `draft`。
|
|
72
|
+
- `run_observe`:字段必须在模板 `observables` 里声明过;来源文件必须已在清单登记且指纹一致。
|
|
73
|
+
- `note_write(claim)`:`evidence[]` 每条都必须能解析;解析失败即拒绝。
|
|
74
|
+
- `convention_declare` / `note_adjudicate`(需用户权威时):必须带真实答复,写入时回查会话。
|
|
75
|
+
|
|
76
|
+
**被拒绝时怎么做**:拒绝文本里永远写着「为什么 + 怎么修」。照着修,不要绕道用 write/pwsh 直接改文件——那样只会让下一次调用继续被拒,并且体检会点名。
|
|
77
|
+
|
|
78
|
+
## 四、用户权威的正确姿势
|
|
79
|
+
|
|
80
|
+
需要用户拍板时(约定、取舍、优先级、撤回结论、归档):
|
|
81
|
+
|
|
82
|
+
1. 用 `ask_user_question` 提问,**每个问题给稳定 id**,选项写清楚;
|
|
83
|
+
2. 用 `entity_query record=ask` 取回**提问标识**(`askCallId`)与用户答复原文(问题自己的 id 不是提问标识);
|
|
84
|
+
3. 把标识与答复原文一起传给 `convention_declare` 或 `note_adjudicate`;
|
|
85
|
+
4. 引擎会回查会话:标识必须对应一次真实的 `ask_user_question`,答复必须与用户实际答复一致;
|
|
86
|
+
5. `statement` 里必须体现答复中的硬约束(数字、引号内容、「必须/不得/至少/不超过」)。
|
|
87
|
+
|
|
88
|
+
**禁止**:把用户的沉默、你的推断、或「用户之前说过类似的话」当作裁决;代答、润色、改写答复原文都会被拒绝。
|
|
89
|
+
|
|
90
|
+
## 五、范畴错误清单(出现即违规)
|
|
91
|
+
|
|
92
|
+
| 混淆 | 表现 | 正确做法 |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| 对话 ↔ 证据 | 用「用户说过」当依据 | 先转成 `decision`(约定/裁决)事件 |
|
|
95
|
+
| 状态 ↔ 证据 | 把 run 的 `done` 当结论成立 | 结论要观察值 + 裁决 |
|
|
96
|
+
| 提议 ↔ 约定 | 自己写「铁律」 | 降级为 `proposal`,标 `proposed` |
|
|
97
|
+
| 提议 ↔ 环境 | 把笔记放进 `skills/` 或写进 `project.yaml` | 提议只放 `.kb/notes`;工程笔记由 `view_render` 渲染 |
|
|
98
|
+
| 结论 ↔ 环境 | 用结论改白名单或阶段 | 环境变更只能走声明工具 |
|
|
99
|
+
| 证据 ↔ 结论 | 把数字当判断 | 拆成「观察」和「结论」两条记录 |
|
|
100
|
+
| 用户权威 ↔ 证据权威 | 用户拍板改写事实 | 用户权威只覆盖「应该怎样」 |
|
|
101
|
+
|
|
102
|
+
## 六、典型会话节奏
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
project_load → project_verify # 先看现状
|
|
106
|
+
stage_read → 按合法边 stage_goto # 走到该在的阶段
|
|
107
|
+
(需要新能力)script_declare → template_declare
|
|
108
|
+
run_draft → run_launch → job_output # 跑
|
|
109
|
+
run_observe → 数字来自产出文件本身
|
|
110
|
+
note_write(claim, evidence=[runId]) → note_adjudicate(accept)
|
|
111
|
+
view_render(report) → view_render(notes-skill)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
报告与工程笔记都是**派生视图**:它们由工具从「生效约定 + 已生效结论」渲染,不是知识源,也不要手改。
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 把本包自带的 preset 同步到 $DSH_HOME/.agent-presets/research-engine。
|
|
3
|
+
//
|
|
4
|
+
// 什么时候用它:
|
|
5
|
+
// * 你的 DSH 是 0.1.1-rc.x(启动器会把 agent-presets 的 roots 覆写为「仅随附根」,
|
|
6
|
+
// cordis.patch.yml 注册的自带根不生效);
|
|
7
|
+
// * 或者你不想动 profile 组合,只想让这台机器上有「研究引擎」这个 preset。
|
|
8
|
+
//
|
|
9
|
+
// 用法:
|
|
10
|
+
// node scripts/install.mjs # 目标不存在 → 安装;内容相同 → 跳过;不同 → 拒绝并提示
|
|
11
|
+
// node scripts/install.mjs --force # 先把已有副本备份成 research-engine.bak-<时间戳>,再覆盖
|
|
12
|
+
// node scripts/install.mjs --dry-run # 只报告将要做什么
|
|
13
|
+
// node scripts/install.mjs --dir <目录> # 指定 preset 根(默认 $DSH_HOME/.agent-presets)
|
|
14
|
+
//
|
|
15
|
+
// 退出码:0 成功/已是最新;1 需要 --force 或出错。
|
|
16
|
+
|
|
17
|
+
import { cp, mkdir, readFile, readdir, rename, stat } from 'node:fs/promises';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
import { join, resolve } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
|
|
23
|
+
const PRESET_ID = 'research-engine';
|
|
24
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
25
|
+
const SOURCE = join(PACKAGE_ROOT, 'presets', PRESET_ID);
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const out = { force: false, dryRun: false, dir: null };
|
|
29
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a === '--force' || a === '-f') out.force = true;
|
|
32
|
+
else if (a === '--dry-run' || a === '-n') out.dryRun = true;
|
|
33
|
+
else if (a === '--dir') { out.dir = argv[i + 1] ?? null; i += 1; }
|
|
34
|
+
else if (a === '--help' || a === '-h') out.help = true;
|
|
35
|
+
else if (a.startsWith('-')) { console.error(`未知参数:${a}`); process.exit(1); }
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const args = parseArgs(process.argv.slice(2));
|
|
41
|
+
if (args.help) {
|
|
42
|
+
console.log('用法:node scripts/install.mjs [--force] [--dry-run] [--dir <preset 根目录>]');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dshHome = process.env.DSH_HOME || join(os.homedir(), '.dsh');
|
|
47
|
+
const presetRoot = resolve(args.dir ?? join(dshHome, '.agent-presets'));
|
|
48
|
+
const target = join(presetRoot, PRESET_ID);
|
|
49
|
+
|
|
50
|
+
/** 递归列出一个目录下所有文件的相对路径(排序,稳定)。 */
|
|
51
|
+
async function listFiles(root, prefix = '') {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const entry of await readdir(join(root, prefix), { withFileTypes: true })) {
|
|
54
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
55
|
+
if (entry.isDirectory()) out.push(...await listFiles(root, rel));
|
|
56
|
+
else if (entry.isFile()) out.push(rel);
|
|
57
|
+
}
|
|
58
|
+
return out.sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 目录内容指纹:相对路径 + 内容,任一字节变化都会改变它。 */
|
|
62
|
+
async function fingerprint(root) {
|
|
63
|
+
const hash = createHash('sha256');
|
|
64
|
+
for (const rel of await listFiles(root)) {
|
|
65
|
+
hash.update(rel).update('\0');
|
|
66
|
+
hash.update(await readFile(join(root, rel)));
|
|
67
|
+
hash.update('\0');
|
|
68
|
+
}
|
|
69
|
+
return hash.digest('hex');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function exists(p) { return stat(p).then(() => true, () => false); }
|
|
73
|
+
|
|
74
|
+
if (!await exists(SOURCE)) {
|
|
75
|
+
console.error(`✗ 找不到本包自带的 preset:${SOURCE}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sourceHash = await fingerprint(SOURCE);
|
|
80
|
+
const installed = await exists(target);
|
|
81
|
+
const installedHash = installed ? await fingerprint(target) : null;
|
|
82
|
+
|
|
83
|
+
console.log(`preset 根:${presetRoot}`);
|
|
84
|
+
console.log(`目标目录:${target}`);
|
|
85
|
+
console.log(`本包内容:${sourceHash.slice(0, 12)}`);
|
|
86
|
+
|
|
87
|
+
if (installedHash === sourceHash) {
|
|
88
|
+
console.log('✓ 目标已是同一份内容,无需安装。');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (installed && !args.force) {
|
|
93
|
+
console.log(`! 目标已存在且内容不同(现有 ${installedHash.slice(0, 12)})。`);
|
|
94
|
+
console.log(' 可能是你本地改过的副本。确认要覆盖就加 --force(会先备份)。');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const backup = installed ? `${target}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}` : null;
|
|
99
|
+
if (args.dryRun) {
|
|
100
|
+
console.log(`· 将创建目录 ${presetRoot}`);
|
|
101
|
+
if (backup) console.log(`· 将备份 ${target} → ${backup}`);
|
|
102
|
+
console.log(`· 将复制 ${SOURCE} → ${target}`);
|
|
103
|
+
console.log('(--dry-run:未做任何改动)');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
await mkdir(presetRoot, { recursive: true });
|
|
108
|
+
if (backup) {
|
|
109
|
+
await rename(target, backup);
|
|
110
|
+
console.log(`· 已备份旧副本 → ${backup}`);
|
|
111
|
+
}
|
|
112
|
+
await cp(SOURCE, target, { recursive: true });
|
|
113
|
+
console.log(`✓ 已安装 ${PRESET_ID} → ${target}`);
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log('下一步:在 DSH 里新建会话,选「研究引擎」preset(无需重启)。');
|
|
116
|
+
console.log('若选择器里没出现,先确认 $DSH_HOME 与 dsh 进程用的是同一个 home。');
|
package/startup.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// research-engine 组合包的提供方行(profile 层,非 preset 层)。
|
|
2
|
+
//
|
|
3
|
+
// 职责只有一件事:把「本包自带的 preset 根」暴露成一个服务,供 cordis.patch.yml 里
|
|
4
|
+
// agent-presets 行的 !!js 配置读取 —— 这样 roots 的路径来自本包自己的 import.meta.url,
|
|
5
|
+
// 不依赖 cwd,也不依赖加载器把 baseUrl 指向哪里。
|
|
6
|
+
//
|
|
7
|
+
// 不注册工具、不写文件、不依赖任何 @deepseek-ai/* 包(只用 node: 内建)。
|
|
8
|
+
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
export const name = 'research-engine-bundle';
|
|
12
|
+
|
|
13
|
+
/** 本 preset 的 id(目录名),与 presets/ 下的目录名一致。 */
|
|
14
|
+
export const PRESET_ID = 'research-engine';
|
|
15
|
+
|
|
16
|
+
export function apply(ctx) {
|
|
17
|
+
const packageRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
18
|
+
const presetsDir = fileURLToPath(new URL('presets/', import.meta.url));
|
|
19
|
+
ctx.provide('researchEngineBundle', {
|
|
20
|
+
packageRoot,
|
|
21
|
+
presetsDir,
|
|
22
|
+
presetId: PRESET_ID,
|
|
23
|
+
});
|
|
24
|
+
}
|