@nightpainters/dsh-ling 1.5.1

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 (51) hide show
  1. package/ACCESS-DESIGN.md +119 -0
  2. package/CHANGELOG.md +627 -0
  3. package/DESIGN.md +461 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +7 -0
  6. package/README.md +236 -0
  7. package/cordis.patch.yml +16 -0
  8. package/lib/audit/access-log.js +533 -0
  9. package/lib/audit/index.js +92 -0
  10. package/lib/audit/patch-spec.js +195 -0
  11. package/lib/client.js +5069 -0
  12. package/lib/host/api.js +2584 -0
  13. package/lib/host/archive.js +194 -0
  14. package/lib/host/autotree.js +131 -0
  15. package/lib/host/backfill.js +267 -0
  16. package/lib/host/classify.js +10 -0
  17. package/lib/host/clock-skill.js +141 -0
  18. package/lib/host/clock.js +132 -0
  19. package/lib/host/corpus-suggest.js +139 -0
  20. package/lib/host/deepsummary.js +338 -0
  21. package/lib/host/dsweb-summary.js +225 -0
  22. package/lib/host/feedback.js +119 -0
  23. package/lib/host/freeze.js +144 -0
  24. package/lib/host/genesis.js +176 -0
  25. package/lib/host/guard.js +184 -0
  26. package/lib/host/habit-gen.js +273 -0
  27. package/lib/host/import-file.js +291 -0
  28. package/lib/host/import-log.js +129 -0
  29. package/lib/host/inject-common.js +25 -0
  30. package/lib/host/inject.js +285 -0
  31. package/lib/host/l1.js +282 -0
  32. package/lib/host/lifecycle.js +367 -0
  33. package/lib/host/memory.js +2389 -0
  34. package/lib/host/mode.js +101 -0
  35. package/lib/host/persona.js +214 -0
  36. package/lib/host/retitle.js +214 -0
  37. package/lib/host/rules.js +195 -0
  38. package/lib/host/scan-dsweb.js +111 -0
  39. package/lib/host/seal.js +17 -0
  40. package/lib/host/settings-file.js +49 -0
  41. package/lib/host/snapshot.js +168 -0
  42. package/lib/host/suggestions.js +47 -0
  43. package/lib/host/summarizer.js +103 -0
  44. package/lib/host/tone-advice.js +112 -0
  45. package/lib/host/tools.js +1067 -0
  46. package/lib/host/tree-ops.js +330 -0
  47. package/lib/host/util.js +165 -0
  48. package/lib/host/zstd.js +63 -0
  49. package/lib/index.js +263 -0
  50. package/package.json +69 -0
  51. package/tools/apply-access-log-patch.mjs +295 -0
@@ -0,0 +1,195 @@
1
+ // E4 访问日志 · 核心补丁规格(唯一真源)
2
+ //
3
+ // 为什么要有这个文件:补丁是一条**插进 node_modules 的 core 代码**里的行,
4
+ // 而"该插什么、插在哪、怎么认出来"必须只有一处定义 —— 否则
5
+ // 补丁脚本与插件侧的"补丁在不在"检查会各自漂移。
6
+ //
7
+ // 补丁形态的原则(E4 任务书 §3.5 的延伸):
8
+ // · **一行 hooks**:core 只多一行 `globalThis.__dshAccessLogXxx?.(…)`,
9
+ // 实现全在本插件里 ⇒ DSH 升级后重打一次,逻辑零改动。
10
+ // · **可选链兜底**:插件没装 / 被卸载时,这一行是 no-op,DSH 照常启动。
11
+ // · **只观察不判定**:补丁不做任何判断、不改任何返回值。
12
+ // · **锚点随 cohort 带**:同一处 core 代码在不同 DSH 版本里写法会变(见第二个目标),
13
+ // 所以目标可以给**候选锚点列表**:逐个试,恰好唯一命中的那个才可用 —— 单锚点必然顾此失彼。
14
+
15
+ /** 补丁标记:用于识别"这一行是我们插的"(幂等 + 可回滚的依据)。 */
16
+ export const PATCH_MARKER = 'E4 access log (dsh-ling patch)';
17
+
18
+ export const OBSERVE_GLOBAL = '__dshAccessLogObserve';
19
+ export const UPGRADE_GLOBAL = '__dshAccessLogUpgrade';
20
+
21
+ /** 补丁状态文件(相对 $DSH_HOME):插件启动时据此报告"补丁在不在"。 */
22
+ export const PATCH_STATE_FILE = 'access-log.patch.json';
23
+
24
+ /** 锚点在一份源码里命中几次(把正则 g 化后统计;不改调用方传进来的正则对象,免留 lastIndex)。 */
25
+ export function countAnchorHits(source, anchor) {
26
+ const re = new RegExp(anchor.source, anchor.flags.includes('g') ? anchor.flags : `${anchor.flags}g`);
27
+ return (source.match(re) || []).length;
28
+ }
29
+
30
+ /**
31
+ * 把一条目标规格归一成**候选锚点数组** —— 新形态 `anchors: [{ cohort, anchor, line, indentDelta }]`
32
+ * 与旧形态(顶层 `anchor` / `line` / `indentDelta`)之间的兼容层。旧形态视作"只有一个候选",
33
+ * 于是不需要 cohort 概念的目标照旧写法即可(两个消费方共用这一个归一函数,判据不会分叉)。
34
+ * `indentDelta` 的兜底沿用旧行为(1 = 进块内一级;同级插入要显式写 0)。
35
+ */
36
+ export function anchorCandidates(target) {
37
+ const raw = Array.isArray(target.anchors) && target.anchors.length
38
+ ? target.anchors
39
+ : [{ cohort: 'legacy', anchor: target.anchor, line: target.line, indentDelta: target.indentDelta }];
40
+ return raw.map((c) => ({
41
+ ...c,
42
+ cohort: c.cohort || 'unknown',
43
+ indentDelta: Number.isInteger(c.indentDelta)
44
+ ? c.indentDelta
45
+ : (Number.isInteger(target.indentDelta) ? target.indentDelta : 1),
46
+ }));
47
+ }
48
+
49
+ /**
50
+ * 在一份文件内容上解析"该用哪个候选锚点"(**纯函数**,不碰文件系统;补丁脚本与单测共用同一判据)。
51
+ *
52
+ * 语义(纪律:宁可不动,也不能在 core 里插错地方):
53
+ * · `hit` —— 恰好一个候选唯一命中 ⇒ 可用(`candidate` 带着该 cohort 自己的 `line`);
54
+ * · `ambiguous` —— 两个及以上候选各自唯一命中(两种 cohort 的写法同时在场)⇒ **拒绝改动,不许猜**;
55
+ * · `drifted` —— 没有任何候选唯一命中(全漂,或某个候选命中 2 次以上)⇒ 需要人工更新锚点。
56
+ * `counts` 逐候选报命中数,供脚本打印与人工核对。
57
+ */
58
+ export function resolveAnchor(target, source) {
59
+ const counts = anchorCandidates(target)
60
+ .map((c) => ({ cohort: c.cohort, candidate: c, hits: countAnchorHits(source, c.anchor) }));
61
+ const usable = counts.filter((x) => x.hits === 1);
62
+ if (usable.length === 1) return { state: 'hit', cohort: usable[0].cohort, candidate: usable[0].candidate, counts };
63
+ if (usable.length > 1) return { state: 'ambiguous', cohort: null, candidate: null, counts };
64
+ return { state: 'drifted', cohort: null, candidate: null, counts };
65
+ }
66
+
67
+ /**
68
+ * 一行是不是**我们插进去的钩子行**(带标记 + `globalThis.__dshAccessLog…?.(` 可选调用)。
69
+ * 回滚(删行)与 `--check` 复核都用它 —— 判据只此一处,免得两处各自漂移。
70
+ */
71
+ export function isHookLine(line) {
72
+ return (
73
+ typeof line === 'string' &&
74
+ line.includes(PATCH_MARKER) &&
75
+ line.includes('globalThis.__dshAccessLog')
76
+ );
77
+ }
78
+
79
+ /** 删掉所有钩子行(回滚)。只删"带标记且形如钩子调用"的行,绝不误伤别的代码。 */
80
+ export function stripHookLines(source) {
81
+ return String(source).split(/\r?\n/).filter((line) => !isHookLine(line)).join('\n');
82
+ }
83
+
84
+ /**
85
+ * **已打补丁的文件是否仍然有效**(C-09,2026-09-26)—— `--check` 的判据,纯函数,不碰文件系统。
86
+ *
87
+ * 为什么必须复核:旧判据是 `source.includes(PATCH_MARKER)`("文件里出现过标记就算 applied")。
88
+ * 那是个**绿色短路**:标记行还在、但锚点那几行已经被改掉的文件(DSH 升级改写了这段代码、
89
+ * 或有人手工动过)照样报 `applied` 并退出 0 ⇒ 用户以为 E4 在记,实际一行都不会记。
90
+ * 判据必须落在**实体**上:插入点那几行还在不在原处、插进去的那行是不是**本站 cohort 的**文本。
91
+ *
92
+ * 三合一(缺一即不算 applied):
93
+ * ① 有钩子行;② 锚点仍**恰好唯一**命中(`resolveAnchor` 同判据);③ 钩子行正文 = 该 cohort 期望的插入行正文
94
+ * (缩进不参与比较:缩进错了 JS 照样跑,cohort 错了就是 ReferenceError —— 后者才是要抓的)。
95
+ *
96
+ * 返回 `{ state, cohort, candidate, counts, expected, actual, hooks }`:
97
+ * · `applied` —— 三条全过;
98
+ * · `cohort-mismatch` —— 锚点在、钩子行却是另一个 cohort 的文本(0.1.5 的 `rejection` 行插进 0.1.7 的文件 ⇒ 每个请求都炸);
99
+ * · `stale` —— 钩子行在,但锚点已不在原处(`drifted` / `ambiguous`);
100
+ * · `duplicated` —— 钩子行不止一条(重复打过补丁 ⇒ 每行日志记两遍);
101
+ * · `not-applied` —— 一条钩子行都没有(此时 `cohort`/`counts` 来自 `resolveAnchor`,供"能不能打"参考)。
102
+ */
103
+ export function verifyPatched(target, source) {
104
+ const text = String(source);
105
+ const hooks = text.split(/\r?\n/).filter(isHookLine);
106
+ const res = resolveAnchor(target, text);
107
+ const counts = res.counts;
108
+ if (!hooks.length) {
109
+ return { state: 'not-applied', cohort: res.cohort, candidate: null, counts, expected: null, actual: null, hooks: 0 };
110
+ }
111
+ if (hooks.length > 1) {
112
+ return { state: 'duplicated', cohort: res.cohort, candidate: null, counts, expected: null, actual: hooks[0].trim(), hooks: hooks.length };
113
+ }
114
+ if (res.state !== 'hit') {
115
+ return { state: 'stale', cohort: null, candidate: null, counts, expected: null, actual: hooks[0].trim(), hooks: 1 };
116
+ }
117
+ const expected = res.candidate.line('').trim();
118
+ const actual = hooks[0].trim();
119
+ return {
120
+ state: actual === expected ? 'applied' : 'cohort-mismatch',
121
+ cohort: res.cohort,
122
+ candidate: res.candidate,
123
+ counts,
124
+ expected,
125
+ actual,
126
+ hooks: 1,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * dsh-api-gateway 的 `/api/remote.mux` upgrade:**同一处代码在不同 DSH cohort 里写法不同**,
132
+ * 所以这里是**候选锚点列表**(逐个试、恰好唯一命中的那个才可用),而不是单一正则。
133
+ *
134
+ * 为什么必须多候选:DSH 0.1.7 起 `requestRejection()` 不再公开,改成 `admit(req)` 返回
135
+ * `{ peer }` 或 `{ rejection: <status> }`;两个 cohort 的**钩子行文本也不同**(旧的要直接交
136
+ * `rejection` 变量,新的得从 `admission` 里取)。只留新锚点 ⇒ 还在 0.1.5 的用户锚点失配 ⇒
137
+ * 补丁打不上 ⇒ **日志静默不记**(这正是"锚点必须随 cohort 带着走"的原因)。
138
+ */
139
+ const MUX_ANCHORS = [
140
+ {
141
+ cohort: '0.1.7+',
142
+ // dsh-client-connection/lib/index.js:admit() 返回 { peer: operator } 或 { rejection: <status> }。
143
+ // 语义等价:允许通过时把 undefined 交给钩子、被拦下时交给它 status —— 与 0.1.5 的写法一致。
144
+ anchor: /^([ \t]*)const admission = webCtx\.connection\.admit\(req\);\r?\n/m,
145
+ indentDelta: 0,
146
+ line: (indent) =>
147
+ `${indent}globalThis.${UPGRADE_GLOBAL}?.(req, "rejection" in admission ? admission.rejection : void 0); // ${PATCH_MARKER}\n`,
148
+ },
149
+ {
150
+ cohort: '0.1.5',
151
+ anchor: /^([ \t]*)const rejection = webCtx\.connection\.requestRejection\(req\);\r?\n/m,
152
+ indentDelta: 0,
153
+ // 这里**只能**交 `rejection`:0.1.5 的文件里没有 `admission` 这个名字,
154
+ // 写成 admission 的话每个 upgrade 请求都会 ReferenceError(`?.` 保不住未声明的标识符)。
155
+ line: (indent) => `${indent}globalThis.${UPGRADE_GLOBAL}?.(req, rejection); // ${PATCH_MARKER}\n`,
156
+ },
157
+ ];
158
+
159
+ /**
160
+ * 两条补丁的目标。插入位置由锚点决定:命中后把 `line(indent)` 插到锚点的**最后一行之后**。
161
+ * `indentDelta`:插入行相对锚点最后一行的缩进偏移(进块内 +1,同级 0)。
162
+ *
163
+ * 锚点形态两种(`anchorCandidates()` 归一,判据见 `resolveAnchor()`):
164
+ * · 单一锚点 —— 顶层 `anchor` / `line`:该正则必须**在目标文件里唯一命中**;
165
+ * · 候选锚点 —— `anchors: [{ cohort, anchor, line, indentDelta }]`:逐 cohort 试,恰好一个唯一命中才动手;
166
+ * 多个同时命中 ⇒ 歧义(拒绝改动);全不命中 ⇒ 漂移(退出码 2,等人工更新锚点)。
167
+ */
168
+ export const PATCH_TARGETS = [
169
+ {
170
+ id: 'client-connection-api',
171
+ label: 'dsh-client-connection · /api 唯一入口(403/401 栅栏就在里面)',
172
+ packageDir: ['node_modules', '@deepseek-ai', 'dsh-client-connection'],
173
+ file: 'lib/index.js',
174
+ anchor: /^([ \t]*)path: API_PATH,\r?\n([ \t]*)handler: async \(req, res\) => \{\r?\n/m,
175
+ indentDelta: 1,
176
+ line: (indent) => `${indent}globalThis.${OBSERVE_GLOBAL}?.(req, res); // ${PATCH_MARKER}\n`,
177
+ },
178
+ {
179
+ id: 'api-gateway-mux',
180
+ label: 'dsh-api-gateway · /api/remote.mux upgrade(同一条栅栏的第二个入口,锚点随 cohort 漂移)',
181
+ packageDir: ['node_modules', '@deepseek-ai', 'dsh-api-gateway'],
182
+ file: 'lib/index.js',
183
+ anchors: MUX_ANCHORS,
184
+ // 顶层 `anchor` / `line` 是**派生**的(= 第一个候选),只为老读法兜底(插件侧状态检查、
185
+ // tests/access-log.test.mjs 读的都是顶层字段)—— 绝不在这里手写第二份,免得跟 `anchors` 漂移。
186
+ anchor: MUX_ANCHORS[0].anchor,
187
+ line: MUX_ANCHORS[0].line,
188
+ indentDelta: MUX_ANCHORS[0].indentDelta,
189
+ },
190
+ ];
191
+
192
+ /** 目标文件相对 DSH 包根的路径。 */
193
+ export function targetPath(dshRoot, target) {
194
+ return [dshRoot, ...target.packageDir, target.file].join(process.platform === 'win32' ? '\\' : '/');
195
+ }