@mingmaple/dsh-handover 0.0.0-stage → 0.9.6

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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -2
  3. package/docs/migrate.md +61 -0
  4. package/docs/troubleshoot.md +48 -0
  5. package/package.json +55 -3
  6. package/scripts/blank-dsh.mjs +203 -0
  7. package/scripts/blank-smoke.mjs +820 -0
  8. package/scripts/build.mjs +58 -0
  9. package/scripts/check-docs.mjs +74 -0
  10. package/scripts/e2e-migration.mjs +127 -0
  11. package/scripts/lib/register.mjs +103 -0
  12. package/src/client.js +887 -0
  13. package/src/cordis.patch.yml +16 -0
  14. package/src/index.mjs +631 -0
  15. package/src/lib/absrefs.mjs +112 -0
  16. package/src/lib/api.mjs +234 -0
  17. package/src/lib/archive.mjs +198 -0
  18. package/src/lib/brief.mjs +74 -0
  19. package/src/lib/charter.mjs +28 -0
  20. package/src/lib/config.mjs +36 -0
  21. package/src/lib/consent.mjs +84 -0
  22. package/src/lib/drift.mjs +65 -0
  23. package/src/lib/events.mjs +89 -0
  24. package/src/lib/extra-prompt.mjs +41 -0
  25. package/src/lib/facts.mjs +70 -0
  26. package/src/lib/gates.mjs +148 -0
  27. package/src/lib/guide.mjs +99 -0
  28. package/src/lib/handover-io.mjs +49 -0
  29. package/src/lib/lint.mjs +152 -0
  30. package/src/lib/manual.mjs +124 -0
  31. package/src/lib/mover.mjs +663 -0
  32. package/src/lib/names.mjs +33 -0
  33. package/src/lib/output.mjs +11 -0
  34. package/src/lib/pack-ops.mjs +329 -0
  35. package/src/lib/peer.mjs +260 -0
  36. package/src/lib/phase.mjs +34 -0
  37. package/src/lib/profile.mjs +204 -0
  38. package/src/lib/registry.mjs +53 -0
  39. package/src/lib/retarget-helper.tmpl +46 -0
  40. package/src/lib/role.mjs +217 -0
  41. package/src/lib/rpc-carrier.mjs +65 -0
  42. package/src/lib/scaffold.mjs +118 -0
  43. package/src/lib/schedule.mjs +227 -0
  44. package/src/lib/session-ops.mjs +362 -0
  45. package/src/lib/sessions.mjs +142 -0
  46. package/src/lib/sevenzip.mjs +48 -0
  47. package/src/lib/status.mjs +171 -0
  48. package/src/lib/text.mjs +74 -0
  49. package/src/lib/webroute.mjs +130 -0
  50. package/src/lib/workspace.mjs +23 -0
  51. package/src/lib/zstd-log.mjs +65 -0
  52. package/src/selftest.mjs +84 -0
  53. package/src/templates/Changelog.skeleton.md +10 -0
  54. package/src/templates/Charter.skeleton.md +53 -0
  55. package/src/templates/Handover.global.skeleton.md +56 -0
  56. package/src/templates/Prompt_Templates.md +96 -0
  57. package/src/templates/Runtime_Manual.md +166 -0
  58. package/src/templates/rules/Handover_Rules.md +43 -0
  59. package/src/templates/rules/Migration_Rules.md +32 -0
  60. package/src/templates/rules/Misc_Task_Standard.md +23 -0
  61. package/src/templates/rules/Writing_Rules.md +24 -0
  62. package/src/tools/ho_brief.mjs +57 -0
  63. package/src/tools/ho_changelog.mjs +59 -0
  64. package/src/tools/ho_charter.mjs +89 -0
  65. package/src/tools/ho_checkpoint.mjs +125 -0
  66. package/src/tools/ho_diag.mjs +85 -0
  67. package/src/tools/ho_drift.mjs +41 -0
  68. package/src/tools/ho_gate.mjs +69 -0
  69. package/src/tools/ho_handover.mjs +83 -0
  70. package/src/tools/ho_init.mjs +82 -0
  71. package/src/tools/ho_pack.mjs +38 -0
  72. package/src/tools/ho_phase.mjs +37 -0
  73. package/src/tools/ho_profile.mjs +97 -0
  74. package/src/tools/ho_sessions.mjs +114 -0
  75. package/src/tools/ho_snapshot.mjs +103 -0
  76. package/src/tools/ho_tokens.mjs +57 -0
  77. package/src/tools/ho_workspace.mjs +213 -0
  78. package/src/tools/peer_list.mjs +37 -0
  79. package/src/tools/peer_new.mjs +75 -0
  80. package/src/tools/peer_peek.mjs +41 -0
  81. package/src/tools/peer_rename.mjs +31 -0
  82. package/src/tools/peer_send.mjs +52 -0
  83. package/src/tools/peer_status.mjs +46 -0
@@ -0,0 +1,49 @@
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
+ * 写交接:先把旧文件复制到 <作用域>/Snapshots/pm-handover-<stamp>/,再原子替换,最后按角色体检。
22
+ * @param {object} [opts] { info?: 角色信息(决定体检判据与备份目录), kind?: 'global'|'project'|'misc' }
23
+ * @returns {{ path, backup, verdict, reasons, bytes }}
24
+ */
25
+ export function writeHandover(ws, file, text, mode, opts) {
26
+ const o = opts || {};
27
+ const info = o.info || { role: o.kind === 'project' ? 'project' : o.kind === 'misc' ? 'misc' : 'global' };
28
+ let dir = null;
29
+ try { dir = snapshotDirFor(ws, info); } catch { dir = path.join(ws, '_Temp', '_Global', 'Snapshots'); }
30
+ let backup = null;
31
+ try {
32
+ if (fs.existsSync(file)) {
33
+ const bdir = path.join(dir, "pm-handover-" + stamp());
34
+ fs.mkdirSync(bdir, { recursive: true });
35
+ backup = path.join(bdir, path.basename(file));
36
+ fs.copyFileSync(file, backup);
37
+ }
38
+ } catch (e) { throw new Error("备份失败,已中止写入:" + e.message); }
39
+ const body = mode === "append" && fs.existsSync(file) ? readUtf8(file) + String.fromCharCode(10) + text : text;
40
+ const parent = path.dirname(file);
41
+ fs.mkdirSync(parent, { recursive: true });
42
+ const tmp = file + ".tmp-" + stamp();
43
+ fs.writeFileSync(tmp, body, "utf8");
44
+ fs.renameSync(tmp, file);
45
+ const res = (o.kind === 'project' || o.kind === 'misc' || info.role === 'project' || info.role === 'misc')
46
+ ? lintProject(file)
47
+ : lintGlobal(file);
48
+ return { path: file, backup, verdict: res.verdict, reasons: res.reasons, bytes: Buffer.byteLength(body, "utf8") };
49
+ }
@@ -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),轮末自动压缩并续跑;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)。',
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
+ }