@mingmaple/dsh-handover 0.0.0-stage → 0.9.12
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/LICENSE +21 -0
- package/README.md +149 -2
- package/docs/migrate.md +61 -0
- package/docs/troubleshoot.md +77 -0
- package/package.json +70 -5
- package/scripts/blank-dsh.mjs +203 -0
- package/scripts/blank-smoke.mjs +981 -0
- package/scripts/build.mjs +61 -0
- package/scripts/check-docs.mjs +74 -0
- package/scripts/e2e-migration.mjs +127 -0
- package/scripts/lib/register.mjs +103 -0
- package/src/client.js +945 -0
- package/src/cordis.patch.yml +16 -0
- package/src/index.mjs +726 -0
- package/src/lib/absrefs.mjs +112 -0
- package/src/lib/api.mjs +291 -0
- package/src/lib/archive.mjs +198 -0
- package/src/lib/brief.mjs +74 -0
- package/src/lib/charter.mjs +28 -0
- package/src/lib/config.mjs +37 -0
- package/src/lib/consent.mjs +84 -0
- package/src/lib/drift.mjs +65 -0
- package/src/lib/events.mjs +89 -0
- package/src/lib/extra-prompt.mjs +41 -0
- package/src/lib/facts.mjs +70 -0
- package/src/lib/gates.mjs +148 -0
- package/src/lib/guide.mjs +103 -0
- package/src/lib/handover-io.mjs +69 -0
- package/src/lib/lint.mjs +152 -0
- package/src/lib/manual.mjs +124 -0
- package/src/lib/mover.mjs +663 -0
- package/src/lib/names.mjs +33 -0
- package/src/lib/output.mjs +11 -0
- package/src/lib/pack-ops.mjs +332 -0
- package/src/lib/peer.mjs +260 -0
- package/src/lib/phase.mjs +36 -0
- package/src/lib/profile.mjs +204 -0
- package/src/lib/prompt.mjs +18 -0
- package/src/lib/registry.mjs +53 -0
- package/src/lib/retarget-helper.tmpl +46 -0
- package/src/lib/role.mjs +251 -0
- package/src/lib/rpc-carrier.mjs +65 -0
- package/src/lib/scaffold.mjs +118 -0
- package/src/lib/schedule.mjs +223 -0
- package/src/lib/session-create.mjs +42 -0
- package/src/lib/session-ops.mjs +408 -0
- package/src/lib/sessions.mjs +143 -0
- package/src/lib/sevenzip.mjs +48 -0
- package/src/lib/status.mjs +177 -0
- package/src/lib/text.mjs +101 -0
- package/src/lib/webroute.mjs +130 -0
- package/src/lib/workspace.mjs +23 -0
- package/src/lib/zstd-log.mjs +65 -0
- package/src/selftest.mjs +84 -0
- package/src/templates/Changelog.skeleton.md +10 -0
- package/src/templates/Charter.skeleton.md +53 -0
- package/src/templates/Handover.global.skeleton.md +56 -0
- package/src/templates/Prompt_Templates.md +96 -0
- package/src/templates/Runtime_Manual.md +166 -0
- package/src/templates/rules/Handover_Rules.md +44 -0
- package/src/templates/rules/Migration_Rules.md +32 -0
- package/src/templates/rules/Misc_Task_Standard.md +23 -0
- package/src/templates/rules/Writing_Rules.md +24 -0
- package/src/tools/ho_brief.mjs +57 -0
- package/src/tools/ho_changelog.mjs +59 -0
- package/src/tools/ho_charter.mjs +89 -0
- package/src/tools/ho_checkpoint.mjs +132 -0
- package/src/tools/ho_diag.mjs +87 -0
- package/src/tools/ho_drift.mjs +41 -0
- package/src/tools/ho_gate.mjs +69 -0
- package/src/tools/ho_handover.mjs +84 -0
- package/src/tools/ho_init.mjs +82 -0
- package/src/tools/ho_pack.mjs +38 -0
- package/src/tools/ho_phase.mjs +37 -0
- package/src/tools/ho_profile.mjs +105 -0
- package/src/tools/ho_sessions.mjs +115 -0
- package/src/tools/ho_snapshot.mjs +104 -0
- package/src/tools/ho_tokens.mjs +57 -0
- package/src/tools/ho_workspace.mjs +217 -0
- package/src/tools/peer_list.mjs +37 -0
- package/src/tools/peer_new.mjs +75 -0
- package/src/tools/peer_peek.mjs +41 -0
- package/src/tools/peer_rename.mjs +31 -0
- package/src/tools/peer_send.mjs +52 -0
- package/src/tools/peer_status.mjs +46 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设置页「使用说明」的内容:面向人,不面向模型。
|
|
3
|
+
* 数据放宿主侧,客户端只负责渲染——改文案不用动客户端 bundle。
|
|
4
|
+
* 行文用标准书面语:不用口语化的示例句,示例一律写成可以直接提的要求。
|
|
5
|
+
* 水位与工具数由 facts.mjs、names.mjs 推导,不在这里重复写死。
|
|
6
|
+
*/
|
|
7
|
+
import { compactSummaryText, watermark } from './facts.mjs';
|
|
8
|
+
import { ALL_TOOL_NAMES } from './names.mjs';
|
|
9
|
+
|
|
10
|
+
/** 首次安装、尚未接入工作区时的下一步(设置页与工具输出共用同一份)。 */
|
|
11
|
+
export const SETUP_STEPS = [
|
|
12
|
+
'准备一个长期存在的目录作为全局根的上级目录,不要选临时目录或下载目录。',
|
|
13
|
+
'在本页下方的输入框中粘贴该目录的绝对路径,点「一键部署」:插件会在其中建立 _Workspace 与整套结构,注册为「全局」工作区,并新建一个「全局」会话。',
|
|
14
|
+
'在左侧「全局」工作区中打开新会话即可开始。若只想先铺文件,可点「仅建立文件体系」,之后再注册工作区。',
|
|
15
|
+
'工作区已接入但还没有全局会话时,本页会单独出现「全局会话」卡片,可随时补建。',
|
|
16
|
+
'新建项目或杂项会话时,应当指明目标工作区;找不到工作区时,插件会把已注册的工作区列出来,不会静默落到别处。',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export function buildGuide(cfg) {
|
|
20
|
+
const w = watermark(cfg);
|
|
21
|
+
const compact = compactSummaryText(cfg);
|
|
22
|
+
const toolCount = ALL_TOOL_NAMES.length;
|
|
23
|
+
return {
|
|
24
|
+
intro: [
|
|
25
|
+
'本插件的全部功能都在对话中调用:不必记命令,也不必操作菜单,用中文说明要做的事,全局会话会选择相应的工具执行。',
|
|
26
|
+
'工具与提示按会话角色载入:全局会话可见全部 ' + toolCount + ' 个工具,项目与杂项会话只载入自己需要的几个,普通会话不载入插件工具,也不注入提示段。角色在下方「会话角色」中指定。',
|
|
27
|
+
'本页除首次安装、全局前置提示词与会话角色外均为只读;需要最新读数时,点击右上角的「刷新」。',
|
|
28
|
+
],
|
|
29
|
+
situations: [
|
|
30
|
+
{ when: '首次安装', say: '部署全局文件体系', what: '本页「一键部署」可一次完成三件事:建文件体系、注册「全局」工作区、建「全局」会话' },
|
|
31
|
+
{ when: '作业开始与结束', say: '先出一份起始简报', what: '汇总上下文水位、通用约束条闸门、会话清点与阶段建议(ho_brief)' },
|
|
32
|
+
{ when: '查阅规则', say: '把 Charter 第 7.2.3 节取出来', what: '按节返回规则正文,避免整篇读取(ho_charter;完整流程用 doc="runtime")' },
|
|
33
|
+
{ when: '交接与压缩续跑', say: '先把交接文档写好,再压缩并继续', what: '写自己角色的交接、体检、登记压缩,并由助手选择压缩后去向:继续工作时用 resume=continue,任务已收尾时用 resume=wait(压缩后不唤醒)(ho_handover、ho_checkpoint)' },
|
|
34
|
+
{ when: '接近上下文上限', say: '查一下本会话的上下文水位', what: compact + '(ho_phase)' },
|
|
35
|
+
{ when: '换任', say: '建立下一代全局会话', what: '建会话、唤醒并投递启动提示词;在任会话水位低于 ' + w.warn + ' 时拒绝新建(ho_sessions action=new)' },
|
|
36
|
+
{ when: '会话角色', say: '把 Demo1 设成项目角色,并让它整理自己的文件', what: '写角色表、当场收窄该会话的工具与提示、投递一次性整理指令(ho_sessions action=role / 本页会话角色)' },
|
|
37
|
+
{ when: '通知项目会话', say: '通知 Demo 更新交接文档', what: '投递一条消息;会话之间可直接对话,不必经全局转述;会消耗目标会话一整轮 token,应尽量少用(peer_send)' },
|
|
38
|
+
{ when: '全局前置提示词', say: '把这段设为全局前置提示词', what: '保存到本机;仅在你开启且内容非空时注入全部会话,关闭或留空时不注册提示段' },
|
|
39
|
+
{ when: '项目骨架', say: '注册 Demo 的骨架', what: '读取、注册、校验项目或杂项的骨架档案;由该项目会话自己写文件并注册(ho_profile)' },
|
|
40
|
+
{ when: '查看会话', say: '列出当前活跃的会话', what: '水位、运行状态、活跃或沉默、在任或前任(ho_sessions)' },
|
|
41
|
+
{ when: '打包迁移', say: '把 Demo 打包成迁移包', what: '收集文件、逐文件 sha256、7z、自检、绝对路径体检(ho_pack action=pack)' },
|
|
42
|
+
{ when: '在新机器恢复', say: '预演这次恢复', what: '校验、列出覆盖清单、备份、写入;默认仅预演(ho_pack action=restore)' },
|
|
43
|
+
{ when: '删除会话', say: '把这个会话归档', what: '官方仅提供归档;需要再次唤醒时须先解归档(ho_sessions action=archive、unarchive)' },
|
|
44
|
+
{ when: '建立工作区', say: '把这个目录注册为工作区', what: '注册已存在的目录;删除只删注册,目录与会话日志均保留(ho_workspace)' },
|
|
45
|
+
{ when: '首次安装', say: '把某个目录建成全局根目录', what: '在该目录下建立整套全局文件体系,并注册为工作区(ho_init)' },
|
|
46
|
+
{ when: '自检', say: '检查插件当前状态', what: '服务可解析性、压缩调度器、压缩计数、本会话角色与工具集(ho_diag)' },
|
|
47
|
+
],
|
|
48
|
+
groups: [
|
|
49
|
+
{
|
|
50
|
+
title: '只读工具(不消耗任何会话的 token)',
|
|
51
|
+
tools: [
|
|
52
|
+
['ho_brief', '起始与收束的单条命令:水位、闸门、会话清点与阶段建议'],
|
|
53
|
+
['ho_gate', '通用约束条闸门(按库型判定)与交接文档体检'],
|
|
54
|
+
['ho_sessions', '会话清点;也用于设定角色、建会话、出提示词、归档'],
|
|
55
|
+
['ho_phase', '本会话阶段:水位、已压缩次数、锁状态、建议动作'],
|
|
56
|
+
['ho_charter', '按节返回规则正文;doc="runtime" 取完整流程手册'],
|
|
57
|
+
['ho_drift', '交接漂移体检:Δ 为当前轮与交接轮之差,大于 3 需刷新'],
|
|
58
|
+
['ho_diag', '插件自检:服务、调度器、压缩计数、角色判定与工具集'],
|
|
59
|
+
['ho_tokens', '会话 token 体检:轮数、步数、每步 token、最贵的单条返回'],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
title: '写工具(会改动文件;均带备份或预演)',
|
|
64
|
+
tools: [
|
|
65
|
+
['ho_handover', '读写、追加、归档本会话自己的交接(写前备份、写后按角色体检)'],
|
|
66
|
+
['ho_checkpoint', '写交接、登记压缩,回合结束后自动压缩并自动续跑'],
|
|
67
|
+
['ho_snapshot', '库快照:逐字节复制、sha256 清单、来源说明'],
|
|
68
|
+
['ho_changelog', '按固定表格追加一行台账'],
|
|
69
|
+
['ho_pack', 'action:check 前置闸门、pack 打包、verify 校验、restore 恢复(默认预演)'],
|
|
70
|
+
['ho_workspace', '工作区列表、创建、删除;删除只删注册'],
|
|
71
|
+
['ho_init', '首次安装:在指定目录下建立整套全局文件体系,可顺带注册工作区'],
|
|
72
|
+
['ho_profile', '项目或杂项骨架:读取、注册、校验、取预设'],
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: '平级会话工具',
|
|
77
|
+
tools: [
|
|
78
|
+
['peer_list', '会话清点,只读投影缓存,不激活任何会话'],
|
|
79
|
+
['peer_status', '单会话详情:水位、运行状态、轮数、步数、工作目录'],
|
|
80
|
+
['peer_send', '向指定会话投递一条消息,是唯一会消耗对方 token 的动作'],
|
|
81
|
+
['peer_new', '新建平级会话,可指定工作区与标题'],
|
|
82
|
+
['peer_rename', '只改显示标题,不唤醒目标会话'],
|
|
83
|
+
['peer_peek', '查看指定会话近期的对话,会消耗本会话 token'],
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
rules: [
|
|
88
|
+
'交接线:上下文水位达到 ' + w.warn + ' 时提醒,达到 ' + w.handover + ' 时写交接并压缩,' + w.hard + ' 为硬上限;同一会话最多压缩 ' + w.max + ' 次,第四段改用新建会话。',
|
|
89
|
+
'压缩不可逆:历史会被摘要替换,因此必须先写出合格的交接文档;体检未通过时,插件拒绝安排压缩。',
|
|
90
|
+
'会话之间可直接 peer_send 对话,不必经全局转述;默认沉默仍适用:只在到交接线且已写交接、需用户决定、发现全局口径有误时发一条,任务完成不向全局报告。',
|
|
91
|
+
'压缩后的去向由助手在登记时选择:任务未完成用 continue 续跑,任务已收尾用 wait 静默收尾,等用户派新任务。',
|
|
92
|
+
'交接文件按角色分离:全局写 _Control_Center/records/Global_Handover.md,项目写 _Project_Meta/<项目>/Handover.md,杂项写 _Misc/tasks/<任务>/Handover.md,互不串用。',
|
|
93
|
+
'会话本身不迁移:跨机器、跨平台的连续性依托交接文档。',
|
|
94
|
+
'全局前置提示词属于用户内容,不是插件约束;关闭或留空时不占用注入点位,可与其它提示词插件并存。',
|
|
95
|
+
'项目与杂项的骨架由各自会话注册;构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
96
|
+
'迁移只搬运文件与清单,不改写文件内容:包内写死的绝对路径会以清单形式报出,由用户或项目会话自行修改;库内引用一律使用相对路径。',
|
|
97
|
+
'插件本体为公开件,不含任何用户项目、会话或账号数据;用户数据与角色表都保存在本机。',
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 默认配置下的说明文本;设置页按实际配置调用 buildGuide。 */
|
|
103
|
+
const GUIDE = buildGuide();
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** 交接文档读写:原子写 + 写前备份 + 写后体检(判据复用 lib/lint.mjs)。路径按会话角色解析。 */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readUtf8 } from './text.mjs';
|
|
5
|
+
import { lintGlobal, lintProject } from './lint.mjs';
|
|
6
|
+
import { snapshotDirFor } from './role.mjs';
|
|
7
|
+
|
|
8
|
+
function stamp() {
|
|
9
|
+
const d = new Date();
|
|
10
|
+
const p = (n) => (n < 10 ? "0" + n : "" + n);
|
|
11
|
+
return d.getFullYear() + p(d.getMonth() + 1) + p(d.getDate()) + "-" + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 读交接:{ path, text };文件不存在返回 text: null。 */
|
|
15
|
+
export function readHandover(file) {
|
|
16
|
+
try { return { path: file, text: readUtf8(file) }; }
|
|
17
|
+
catch { return { path: file, text: null }; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 归档时间戳:`YYYYMMDD_HHMMSS`(到秒)。
|
|
22
|
+
* 规范要求时间戳取归档那一刻;只到分钟会在同一分钟内归档两份时撞名,故到秒。
|
|
23
|
+
*/
|
|
24
|
+
export function archiveStamp(d) {
|
|
25
|
+
const x = d || new Date();
|
|
26
|
+
const p = (n) => (n < 10 ? "0" + n : "" + n);
|
|
27
|
+
return x.getFullYear() + p(x.getMonth() + 1) + p(x.getDate()) + "_" + p(x.getHours()) + p(x.getMinutes()) + p(x.getSeconds());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 归档文件名:`<原文件名>_<YYYYMMDD_HHMMSS>.md`。
|
|
32
|
+
* 「原文件名」取源文件主名(不含扩展名):全局 `Global_Handover_…`、项目与杂项 `Handover_…`;
|
|
33
|
+
* 不加链标识——归档目录本身已按链隔离(规范 §4.1)。
|
|
34
|
+
*/
|
|
35
|
+
export function archiveFileName(file, stampText) {
|
|
36
|
+
const base = String(file || "").split(/[\\/]/).pop().replace(/\.md$/i, "");
|
|
37
|
+
return base + "_" + String(stampText) + ".md";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 写交接:先把旧文件复制到 <作用域>/Snapshots/pm-handover-<stamp>/,再原子替换,最后按角色体检。
|
|
42
|
+
* @param {object} [opts] { info?: 角色信息(决定体检判据与备份目录), kind?: 'global'|'project'|'misc' }
|
|
43
|
+
* @returns {{ path, backup, verdict, reasons, bytes }}
|
|
44
|
+
*/
|
|
45
|
+
export function writeHandover(ws, file, text, mode, opts) {
|
|
46
|
+
const o = opts || {};
|
|
47
|
+
const info = o.info || { role: o.kind === 'project' ? 'project' : o.kind === 'misc' ? 'misc' : 'global' };
|
|
48
|
+
let dir = null;
|
|
49
|
+
try { dir = snapshotDirFor(ws, info); } catch { dir = path.join(ws, '_Temp', '_Global', 'Snapshots'); }
|
|
50
|
+
let backup = null;
|
|
51
|
+
try {
|
|
52
|
+
if (fs.existsSync(file)) {
|
|
53
|
+
const bdir = path.join(dir, "pm-handover-" + stamp());
|
|
54
|
+
fs.mkdirSync(bdir, { recursive: true });
|
|
55
|
+
backup = path.join(bdir, path.basename(file));
|
|
56
|
+
fs.copyFileSync(file, backup);
|
|
57
|
+
}
|
|
58
|
+
} catch (e) { throw new Error("备份失败,已中止写入:" + e.message); }
|
|
59
|
+
const body = mode === "append" && fs.existsSync(file) ? readUtf8(file) + String.fromCharCode(10) + text : text;
|
|
60
|
+
const parent = path.dirname(file);
|
|
61
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
62
|
+
const tmp = file + ".tmp-" + stamp();
|
|
63
|
+
fs.writeFileSync(tmp, body, "utf8");
|
|
64
|
+
fs.renameSync(tmp, file);
|
|
65
|
+
const res = (o.kind === 'project' || o.kind === 'misc' || info.role === 'project' || info.role === 'misc')
|
|
66
|
+
? lintProject(file)
|
|
67
|
+
: lintGlobal(file);
|
|
68
|
+
return { path: file, backup, verdict: res.verdict, reasons: res.reasons, bytes: Buffer.byteLength(body, "utf8") };
|
|
69
|
+
}
|
package/src/lib/lint.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** 交接文档体检:项目交接与全局交接。 */
|
|
2
|
+
import { readUtf8, sectionLines, isFile } from './text.mjs';
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const TICK = String.fromCharCode(96);
|
|
6
|
+
const ACCEPTABLE = [
|
|
7
|
+
'<工作区根>', '<项目名>', '<YYYY-MM-DD>', '<绝对路径>', '<版本号>',
|
|
8
|
+
'<YYYYMMDD_HHMMSS>', '<日期>', '<过去日期>', '<任务短名>', '<N>', '<M>', '<id>', '<新 id>',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** 去掉行内代码段,避免把示例里的占位符当残留。 */
|
|
12
|
+
function stripCodeSpans(text) {
|
|
13
|
+
let out = "";
|
|
14
|
+
let inCode = false;
|
|
15
|
+
for (const ch of text) {
|
|
16
|
+
if (ch === TICK) { inCode = !inCode; continue; }
|
|
17
|
+
if (!inCode) out += ch;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function firstPlaceholder(text) {
|
|
23
|
+
const src = stripCodeSpans(text);
|
|
24
|
+
for (let i = 0; i < src.length; i++) {
|
|
25
|
+
if (src[i] !== '<') continue;
|
|
26
|
+
const j = src.indexOf('>', i + 1);
|
|
27
|
+
if (j === -1) break;
|
|
28
|
+
if (j - i > 41) continue;
|
|
29
|
+
const token = src.slice(i, j + 1);
|
|
30
|
+
if (token.indexOf(String.fromCharCode(10)) !== -1) continue;
|
|
31
|
+
if (ACCEPTABLE.indexOf(token) !== -1) continue;
|
|
32
|
+
return token;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const GLOBAL_SECTIONS = [
|
|
38
|
+
'0. 身份与寻址',
|
|
39
|
+
'1. 正在做的事',
|
|
40
|
+
'2. 下一步',
|
|
41
|
+
'3. 待用户决定',
|
|
42
|
+
'4. 用户要求逐条落位',
|
|
43
|
+
'5. 坑点与禁区',
|
|
44
|
+
'6. 关键路径',
|
|
45
|
+
'7. 会话 id 对照',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const GLOBAL_FIELDS = [
|
|
49
|
+
'会话类型', '本任', '链内代数', '接任', '本文件', '冲突以谁为准', '修订',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function hasHeading(lines, sec) {
|
|
53
|
+
return lines.some((l) => l.startsWith('## ') && l.indexOf(sec) !== -1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tableRows(lines) {
|
|
57
|
+
return lines.filter((l) => l.trim().startsWith('|') && l.indexOf('---') === -1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function bulletRows(lines) {
|
|
61
|
+
return lines.filter((l) => l.trim().startsWith('-') || l.trim().startsWith('*'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 全局交接体检:{ verdict, reasons }。 */
|
|
65
|
+
export function lintGlobal(file) {
|
|
66
|
+
if (!isFile(file)) return { verdict: "读不到", reasons: ["文件不存在:" + file] };
|
|
67
|
+
let text = "";
|
|
68
|
+
try { text = readUtf8(file); } catch (e) { return { verdict: "读不到", reasons: ["读取失败:" + e.message] }; }
|
|
69
|
+
const lines = text.split(String.fromCharCode(10));
|
|
70
|
+
const bad = [];
|
|
71
|
+
// §3 标题有新旧两种写法,旧的「等用户拍板」也按通过处理。
|
|
72
|
+
const SEC_ALIAS = { '3. 待用户决定': ['3. 等用户拍板'] }; // 语体豁免:旧文档标题原文,只作兼容匹配
|
|
73
|
+
for (const sec of GLOBAL_SECTIONS) {
|
|
74
|
+
const alt = SEC_ALIAS[sec] || [];
|
|
75
|
+
if (!hasHeading(lines, sec) && !alt.some((a) => hasHeading(lines, a))) bad.push("缺一节:## " + sec);
|
|
76
|
+
}
|
|
77
|
+
const zero = sectionLines(text, '身份与寻址');
|
|
78
|
+
if (!zero) bad.push('缺 ## 0. 身份与寻址');
|
|
79
|
+
else {
|
|
80
|
+
const zt = zero.join(String.fromCharCode(10));
|
|
81
|
+
for (const f of GLOBAL_FIELDS) if (zt.indexOf("| " + f + " |") === -1 && zt.indexOf("|" + f + "|") === -1) bad.push("§0 缺字段:" + f);
|
|
82
|
+
}
|
|
83
|
+
const chainLine = lines.find((l) => l.indexOf('链内代数') !== -1);
|
|
84
|
+
if (!chainLine) bad.push('§0 缺「链内代数」行');
|
|
85
|
+
else {
|
|
86
|
+
const hasDi = chainLine.indexOf('第') !== -1 && chainLine.indexOf('代') !== -1;
|
|
87
|
+
if (!hasDi) bad.push('链内代数不是「第 N 代」格式:' + chainLine.slice(0, 60));
|
|
88
|
+
}
|
|
89
|
+
const s2 = sectionLines(text, '下一步');
|
|
90
|
+
if (!s2) bad.push('缺 ## 2. 下一步');
|
|
91
|
+
else if (tableRows(s2).length <= 1 && bulletRows(s2).length === 0) bad.push('§2 没列出任何条目');
|
|
92
|
+
const s7 = sectionLines(text, '会话 id 对照');
|
|
93
|
+
if (!s7) bad.push('缺 ## 7. 会话 id 对照');
|
|
94
|
+
else {
|
|
95
|
+
if (!s7.some((l) => l.trim().startsWith('|') && l.indexOf('session-') !== -1)) bad.push('§7 没有一行带 session id');
|
|
96
|
+
if (s7.some((l) => l.indexOf('待回填') !== -1)) bad.push('§7 还有「待回填」');
|
|
97
|
+
}
|
|
98
|
+
const ph = firstPlaceholder(text);
|
|
99
|
+
if (ph) bad.push("残留模板占位符:" + ph);
|
|
100
|
+
return { verdict: bad.length === 0 ? "通过" : "不合格", reasons: bad };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 项目交接体检:{ verdict, reasons },verdict ∈ 通过/不合格/缺节/读不到。 */
|
|
104
|
+
export function lintProject(file) {
|
|
105
|
+
if (!isFile(file)) return { verdict: "不合格", reasons: ["缺 Handover.md:" + file] };
|
|
106
|
+
let text = "";
|
|
107
|
+
try { text = readUtf8(file); } catch (e) { return { verdict: "读不到", reasons: ["读取失败:" + e.message] }; }
|
|
108
|
+
const bad = [];
|
|
109
|
+
const gap = [];
|
|
110
|
+
const body = sectionLines(text, '正在做的事');
|
|
111
|
+
if (!body) gap.push("缺 ## 1. 正在做的事 一节(2026-10-12 起的强制项,本项目还没补)");
|
|
112
|
+
else {
|
|
113
|
+
const core = body.filter((l) => l.trim() !== '' && !l.trim().startsWith('>'));
|
|
114
|
+
if (core.length === 0) bad.push("正在做的事 是空节(或只有引用块说明),没写实质内容");
|
|
115
|
+
else if (core.join('').trim().length < 30) bad.push('正在做的事 内容过短,像是应付');
|
|
116
|
+
}
|
|
117
|
+
const nxt = sectionLines(text, '下一步') || sectionLines(text, '待办');
|
|
118
|
+
if (!nxt) bad.push("缺「下一步待办」一节");
|
|
119
|
+
else if (tableRows(nxt).length <= 1 && bulletRows(nxt).length === 0) bad.push("「下一步待办」没列出任何条目");
|
|
120
|
+
const ph = firstPlaceholder(text);
|
|
121
|
+
if (ph) bad.push("残留模板占位符:" + ph);
|
|
122
|
+
if (bad.length) return { verdict: "不合格", reasons: bad };
|
|
123
|
+
if (gap.length) return { verdict: "缺节", reasons: gap };
|
|
124
|
+
return { verdict: "通过", reasons: [] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function renderLintGlobal(file, res) {
|
|
128
|
+
const out = [];
|
|
129
|
+
out.push("交接文档体检(全局) " + file);
|
|
130
|
+
let size = "?";
|
|
131
|
+
try { size = String(statSync(file).size); } catch { /* ignore */ }
|
|
132
|
+
out.push(" Global_Handover.md " + res.verdict + " " + size + " B");
|
|
133
|
+
for (const r of res.reasons) out.push(" - " + r);
|
|
134
|
+
out.push("判定:" + (res.verdict === "通过" ? "结构齐备" : "不合格(" + res.reasons.length + " 项)"));
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function renderLintProject(name, rows) {
|
|
139
|
+
const list = Array.isArray(rows) ? rows : [{ name, file: "", res: rows }];
|
|
140
|
+
const out = [];
|
|
141
|
+
out.push("交接文档体检 项目 " + (name || "全部"));
|
|
142
|
+
let fails = 0;
|
|
143
|
+
let gaps = 0;
|
|
144
|
+
for (const it of list) {
|
|
145
|
+
out.push(" " + it.name + " " + it.res.verdict);
|
|
146
|
+
for (const r of it.res.reasons) out.push(" - " + r);
|
|
147
|
+
if (it.res.verdict === "不合格") fails++;
|
|
148
|
+
else if (it.res.verdict === "缺节") gaps++;
|
|
149
|
+
}
|
|
150
|
+
out.push("判定:" + list.length + " 个项目 —— 不合格 " + fails + " 个(要改)、缺节 " + gaps + " 个(新规范未铺开,属正常)");
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 逐角色的系统提示段与一次性引导。
|
|
3
|
+
* 职责边界:本文件只写会话定位、写入范围、交接路径、水位与换任、向全局汇报。
|
|
4
|
+
* 其他行为约束(不虚构、最小改动、文风等)不属于本插件,由用户按需通过「全局前置提示词」注入。
|
|
5
|
+
* 文字一律标准书面语;代码里不用反引号拼提示词。
|
|
6
|
+
*/
|
|
7
|
+
import { handoverPathFor, scopeLabel } from './role.mjs';
|
|
8
|
+
import { handoverRuleText, watermark } from './facts.mjs';
|
|
9
|
+
|
|
10
|
+
const NL = String.fromCharCode(10);
|
|
11
|
+
|
|
12
|
+
/** 完整流程的指针句(三档角色共用)。 */
|
|
13
|
+
const RUNTIME_POINTER = '规则细节按节取:ho_charter(doc="runtime", section=…)。';
|
|
14
|
+
|
|
15
|
+
/** 全局会话的常驻段:五节骨架(身份、权限、节奏、交接、按需取用)。 */
|
|
16
|
+
export function globalManual(opts) {
|
|
17
|
+
const o = opts || {};
|
|
18
|
+
const lines = [
|
|
19
|
+
'dsh-handover 运行期(全局)',
|
|
20
|
+
'',
|
|
21
|
+
'一、身份',
|
|
22
|
+
'本会话为「全局」管理会话:协调会话、维护规则与交接。',
|
|
23
|
+
'',
|
|
24
|
+
'二、权限',
|
|
25
|
+
'可写 _Control_Center、_Project_Meta 与项目结构本身;Projects 与各项目库内文件只读,不得代改写;项目内作业不代办。',
|
|
26
|
+
'新建会话与设定角色仅属全局;会话之间可直接 peer_send,不必经全局转述;换任须经用户同意。',
|
|
27
|
+
'',
|
|
28
|
+
'三、节奏',
|
|
29
|
+
handoverRuleText(o.cfg),
|
|
30
|
+
'压缩须用户点「压缩授权」,随后写交接并 ho_checkpoint(confirm=true);任务未完成时压缩后续跑,已收尾时传 resume=wait 不再唤醒;confirm 仅表意图,插件只认那次点击。关键步骤传 at_risk=true 暂缓。',
|
|
31
|
+
'',
|
|
32
|
+
'四、交接',
|
|
33
|
+
'全局交接:_Control_Center/records/Global_Handover.md(ho_handover 默认)。',
|
|
34
|
+
'',
|
|
35
|
+
'五、按需取用',
|
|
36
|
+
RUNTIME_POINTER,
|
|
37
|
+
];
|
|
38
|
+
const mv = moveLine(o);
|
|
39
|
+
if (mv) lines.push(mv);
|
|
40
|
+
return lines.join(NL);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 项目与杂项会话的提示段:只写本角色自己的写入范围、交接与水位。 */
|
|
44
|
+
export function scopedManual(info, opts) {
|
|
45
|
+
const o = opts || {};
|
|
46
|
+
const hp = handoverPathFor(o.ws, info);
|
|
47
|
+
const label = scopeLabel(info);
|
|
48
|
+
const lines = [
|
|
49
|
+
'dsh-handover 运行期(' + label + ')',
|
|
50
|
+
info.role === 'project'
|
|
51
|
+
? '只可写本库与元数据目录,不得写 _Control_Center 及其他项目。'
|
|
52
|
+
: '只可写本任务目录与元数据目录,不得写 _Control_Center、Projects 及其他任务。',
|
|
53
|
+
info.role === 'project'
|
|
54
|
+
? '库根:Projects/' + info.project + '/;元数据:_Project_Meta/' + info.project + '/。'
|
|
55
|
+
: '任务目录:_Misc/tasks/' + info.task + '/。',
|
|
56
|
+
info.role === 'project'
|
|
57
|
+
? '骨架:_Project_Meta/' + info.project + '/project.profile.json;未注册时先通读本库并 ho_profile(action=register)。'
|
|
58
|
+
: '骨架:_Misc/tasks/' + info.task + '/task.profile.json;未注册时先通读本任务并 ho_profile(action=register)。',
|
|
59
|
+
info.role === 'project'
|
|
60
|
+
? '临时与归档:_Temp/' + info.project + '/{Snapshots,Scratch}、_Archives/' + info.project + '/。'
|
|
61
|
+
: '临时与归档:_Temp/_Misc/' + info.task + '/Snapshots/、_Archives/_Misc/' + info.task + '/。',
|
|
62
|
+
hp.path
|
|
63
|
+
? '交接:' + hp.path + '(ho_handover 默认)。'
|
|
64
|
+
: '交接路径未解析(缺角色或项目名),请先在设置页设定角色。',
|
|
65
|
+
handoverRuleText(o.cfg) + '压缩须用户点「压缩授权」,再自行 ho_checkpoint(confirm=true);收尾时传 resume=wait 不再唤醒。',
|
|
66
|
+
'会话间可直接 peer_send,不必经全局转述;默认沉默:只在水位 ' + watermark(o.cfg).warn + ' 已写交接、需用户决定、口径有误时向全局报告;任务完成不报全局。',
|
|
67
|
+
RUNTIME_POINTER,
|
|
68
|
+
];
|
|
69
|
+
const mv = moveLine(o);
|
|
70
|
+
if (mv) lines.push(mv);
|
|
71
|
+
return lines.join(NL);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 迁移提示行(只有真的搬过才出现)。 */
|
|
75
|
+
function moveLine(opts) {
|
|
76
|
+
const mv = opts && opts.move;
|
|
77
|
+
if (!mv || mv.phase !== 'switched') return '';
|
|
78
|
+
return '已迁移至 ' + (mv.dst || '?') + ';' + (mv.src || '?') + ' 现为链接,勿再手工迁移。';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function manualFor(info, opts) {
|
|
82
|
+
const role = info && info.role ? info.role : 'plain';
|
|
83
|
+
if (role === 'global') return globalManual(opts);
|
|
84
|
+
if (role === 'project' || role === 'misc') return scopedManual(info, opts);
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 「整理所属文件」一次性指令:设定项目或杂项角色后投递,唤醒它自己做。 */
|
|
89
|
+
export function organizeMessageFor(info, opts) {
|
|
90
|
+
const o = opts || {};
|
|
91
|
+
const role = info && info.role ? info.role : 'plain';
|
|
92
|
+
if (role === 'plain') return '';
|
|
93
|
+
const head = '【dsh-handover 角色设定】本会话已被设定为「' + scopeLabel(info) + '」。请按下列结构核对自身文件:缺失的目录予以建立,位置不当的文件予以归位;既有文件就地保留,不得删除或修改内容。';
|
|
94
|
+
if (role === 'project') {
|
|
95
|
+
const p = info.project;
|
|
96
|
+
return [
|
|
97
|
+
head,
|
|
98
|
+
'先通读本库,判断本项目的大体骨架:代码与文档主体、必需文件、临时与归档位置。构建型、动画型等项目不必套用插件类项目的文档集。',
|
|
99
|
+
'把结论写入 _Project_Meta/' + p + '/project.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
100
|
+
'写入后调用 ho_profile(action=register) 校验并登记;返回缺失项时按清单补齐。',
|
|
101
|
+
'约束:仅可改动本库与 _Project_Meta/' + p + '/;不得改动 _Control_Center 及其他项目;移动较大文件前先执行 ho_snapshot。可用工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
102
|
+
'完成后以一句话向用户说明「注册了哪些骨架项、仍缺哪些」,无需每轮回执。',
|
|
103
|
+
].join(NL);
|
|
104
|
+
}
|
|
105
|
+
if (role === 'misc') {
|
|
106
|
+
const t = info.task;
|
|
107
|
+
return [
|
|
108
|
+
head,
|
|
109
|
+
'先通读本任务目录,判断本任务的骨架:工作文件、规则文件、交接、快照与归档位置。',
|
|
110
|
+
'把结论写入 _Misc/tasks/' + t + '/task.profile.json;字段格式见 ho_profile(action=presets)。',
|
|
111
|
+
'写入后调用 ho_profile(action=register) 校验并登记。',
|
|
112
|
+
'约束:仅可改动 _Misc/tasks/' + t + '/、_Temp/_Misc/' + t + '/、_Archives/_Misc/' + t + '/;_Control_Center、Projects 与其他任务不得触碰。工具:ho_snapshot、ho_handover、ho_phase、ho_checkpoint、ho_profile、peer_send。',
|
|
113
|
+
'完成后以一句话说明改动即可。',
|
|
114
|
+
].join(NL);
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
head,
|
|
118
|
+
'- _Control_Center/{records,rules,user,templates,tools} — 交接、规则、写作规范、模板、脚本。',
|
|
119
|
+
'- _Project_Meta/Registry.md 与各项目元数据目录。',
|
|
120
|
+
'- Projects/、_Misc/tasks/、_Archives/、_Temp/ — 只维护结构,不代写项目库内文件。',
|
|
121
|
+
'约束:项目库内文件由该项目会话自行整理;骨架由项目自己注册,缺则派发,不代写。',
|
|
122
|
+
'完成后以一句话汇报即可。',
|
|
123
|
+
].join(NL);
|
|
124
|
+
}
|