pi-patch-keeper 1.2.11 → 1.3.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 (3) hide show
  1. package/README.md +32 -23
  2. package/index.ts +30 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -13,7 +13,7 @@ pi 配置守卫:codemode 一致性、force 顺序、环境指纹漂移、注
13
13
 
14
14
  | 项 | 说明 |
15
15
  | --- | --- |
16
- | 配置文件 | **无**(只读 settings.json / APPEND_SYSTEM.md) |
16
+ | 配置文件 | `~/.pi/patch-keeper.json`(可省略;`forceTail` 数组) |
17
17
  | 命令行 | /patch-keeper [fix] |
18
18
  | 调用方式 | **全自动**:启动自检并自动修复 force 顺序;fix 手动重跑 |
19
19
 
@@ -25,11 +25,23 @@ pi install npm:pi-patch-keeper
25
25
 
26
26
  ## 配置
27
27
 
28
- 无配置文件(只读 `settings.json` 与 `APPEND_SYSTEM.md`);无环境变量。
28
+ 配置文件 `~/.pi/patch-keeper.json`(可省略);无环境变量。
29
+
30
+ | 键 | 类型 | 默认 | 说明 |
31
+ | --- | --- | --- | --- |
32
+ | `forceTail` | string[] | `["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"]` | 视为 force 型的包名(须排在 packages 末尾);配了非空数组则完全覆盖默认 |
33
+
34
+ 完整配置:
35
+
36
+ ```json
37
+ {
38
+ "forceTail": ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"]
39
+ }
40
+ ```
29
41
 
30
42
  | 状态文件 | 说明 |
31
43
  | --- | --- |
32
- | `~/.pi/patch-keeper.json` | 上次启动的配置指纹(漂移对比) |
44
+ | `~/.pi/patch-keeper-state.json` | 上次启动的配置指纹(漂移对比) |
33
45
  | `~/.pi/patch-keeper-manual.json` | 手册版本 + 内容签名 |
34
46
  | `~/.pi/agent/settings.json.bak-force-order-*` | force 顺序修复前的备份 |
35
47
 
@@ -51,14 +63,11 @@ pi install npm:pi-patch-keeper
51
63
 
52
64
  ```mermaid
53
65
  flowchart TD
54
- A[factory] --> B[读 settings.json 建指纹]
55
- B --> C[session_start]
56
- C --> D{指纹变化?}
57
- D -->|是| E[通知:手册/结论可能过期]
58
- C --> F{force 顺序错?}
59
- F -->|是| G[备份 + 重排 packages]
60
- C --> H[before_agent_start<br/>比对注入手册版本与磁盘]
61
- H -->|不一致| I[通知 + nextTurn 尾部消息<br/>以磁盘为准]
66
+ A[factory:读 settings.json 建指纹] --> B[session_start]
67
+ A --> C[before_agent_start]
68
+ B -->|指纹变化| D[通知:手册 / 结论可能过期]
69
+ B -->|force 顺序错| E[备份 + 重排 packages]
70
+ C -->|注入版本 vs 磁盘不一致| F[通知 + nextTurn 尾部消息<br/>以磁盘为准]
62
71
  ```
63
72
 
64
73
  ## 原理(小白)
@@ -92,7 +101,7 @@ pi configuration guards: codemode consistency, force order, settings fingerprint
92
101
 
93
102
  | Item | Description |
94
103
  | --- | --- |
95
- | Config file | **None** (reads settings.json / APPEND_SYSTEM.md only) |
104
+ | Config file | `~/.pi/patch-keeper.json` (optional; `forceTail` array) |
96
105
  | Commands | /patch-keeper [fix] |
97
106
  | Automation | **Fully automatic**: self-checks at startup and repairs force order; fix re-runs it |
98
107
 
@@ -104,11 +113,15 @@ pi install npm:pi-patch-keeper
104
113
 
105
114
  ## Configuration
106
115
 
107
- No config file (only reads `settings.json` and `APPEND_SYSTEM.md`); no environment variables.
116
+ Config file `~/.pi/patch-keeper.json` (optional); no environment variables.
117
+
118
+ | Key | Type | Default | Description |
119
+ | --- | --- | --- | --- |
120
+ | `forceTail` | string[] | `["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"]` | Packages treated as force-type (must sit last in packages); a non-empty array replaces the default |
108
121
 
109
122
  | State file | Description |
110
123
  | --- | --- |
111
- | `~/.pi/patch-keeper.json` | Settings fingerprint from the previous start (drift compare) |
124
+ | `~/.pi/patch-keeper-state.json` | Settings fingerprint from the previous start (drift compare) |
112
125
  | `~/.pi/patch-keeper-manual.json` | Manual version + content signature |
113
126
  | `~/.pi/agent/settings.json.bak-force-order-*` | Backup before a force-order repair |
114
127
 
@@ -130,15 +143,11 @@ No config file (only reads `settings.json` and `APPEND_SYSTEM.md`); no environme
130
143
 
131
144
  ```mermaid
132
145
  flowchart TD
133
- A[factory] --> B[read settings.json, build fingerprint]
134
- B --> C[session_start]
135
- C --> D{fingerprint changed?}
136
- D -->|yes| E[notify: manuals may be stale]
137
- C --> F{force order wrong?}
138
- F -->|yes| G[backup + reorder packages]
139
- C --> H[before_agent_start<br/>compare injected manual vs disk]
140
- H -->|mismatch| I[notify + nextTurn message<br/>trust disk]
141
-
146
+ A[factory: build fingerprint from settings.json] --> B[session_start]
147
+ A --> C[before_agent_start]
148
+ B -->|fingerprint changed| D[notify: manuals may be stale]
149
+ B -->|force order wrong| E[backup + reorder packages]
150
+ C -->|injected vs disk mismatch| F[notify + nextTurn message<br/>trust disk]
142
151
  ```
143
152
 
144
153
  ## How it works (plain words)
package/index.ts CHANGED
@@ -15,7 +15,8 @@ import * as path from "node:path";
15
15
 
16
16
  const HOME = os.homedir();
17
17
 
18
- const FINGERPRINT = path.join(HOME, ".pi", "patch-keeper.json");
18
+ const CONFIG = path.join(HOME, ".pi", "patch-keeper.json"); // 用户配置(可编辑)
19
+ const FINGERPRINT = path.join(HOME, ".pi", "patch-keeper-state.json"); // 指纹状态(自动维护)
19
20
 
20
21
  const SETTINGS = path.join(HOME, ".pi", "agent", "settings.json");
21
22
  // 手册(注入对齐检测:本轮注入文本的版本行 vs 磁盘版)
@@ -121,25 +122,42 @@ function manualDrift(): string | null {
121
122
  return null;
122
123
  }
123
124
  // 已知盲区:caveman-think-zh 是 user 扩展(不在 packages 数组),它在检测到原版英文段时
124
- // 也会 force;FORCE_TAIL 覆盖不到,触发时本扩展看不到。
125
+ // 也会 force;forceTail 覆盖不到,触发时本扩展看不到。
125
126
  /** force/条件 force 型必须排在 packages 末尾(其后 section 写入会被静默丢弃)。触发概率升序:rtk → cache-optimizer。 */
126
- const FORCE_TAIL = ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"];
127
+ const DEFAULT_FORCE_TAIL = ["npm:pi-rtk-optimizer", "npm:pi-cache-optimizer"];
128
+
129
+ /** 用户配置(~/.pi/patch-keeper.json,可省略):forceTail 决定哪些包被视为 force 型。 */
130
+ export type PkConfig = { forceTail?: string[] };
131
+ export function loadConfig(): PkConfig {
132
+ const raw = safeRead(CONFIG);
133
+ if (raw === null) return {};
134
+ try { const j = JSON.parse(raw) as PkConfig; return j && typeof j === "object" ? j : {}; } catch { return {}; }
135
+ }
136
+ /** force 型包列表(配置优先,缺省用内置两项)。 */
137
+ export function forceTail(): string[] {
138
+ const c = loadConfig();
139
+ const list = Array.isArray(c.forceTail) ? c.forceTail.filter((x): x is string => typeof x === "string" && x.length > 0) : [];
140
+ return list.length ? list : DEFAULT_FORCE_TAIL;
141
+ }
127
142
  export function forceOrderIssues(packages: string[]): string[] {
128
143
  const out: string[] = [];
144
+ const FT = forceTail();
129
145
  const idx = (n: string) => packages.indexOf(n);
130
- const present = FORCE_TAIL.filter((n) => idx(n) >= 0);
146
+ const present = FT.filter((n) => idx(n) >= 0);
131
147
  if (!present.length) return out;
132
148
  const minForce = Math.min(...present.map(idx));
133
- const later = packages.slice(minForce + 1).filter((n) => !FORCE_TAIL.includes(n));
134
- if (later.length) out.push(`force 型之后仍有 ${later.length} 个包(${later.slice(0, 2).join(" / ")})—— 它们的 section 写入会被丢弃;把 ${FORCE_TAIL.join(" / ")} 移到末尾`);
135
- if (idx(FORCE_TAIL[0]) >= 0 && idx(FORCE_TAIL[1]) >= 0 && idx(FORCE_TAIL[0]) > idx(FORCE_TAIL[1])) out.push("force 顺序应为 rtk → cache-optimizer(最可能触发者最后)");
149
+ const later = packages.slice(minForce + 1).filter((n) => !FT.includes(n));
150
+ if (later.length) out.push(`force 型之后仍有 ${later.length} 个包(${later.slice(0, 2).join(" / ")})—— 它们的 section 写入会被丢弃;把 ${FT.join(" / ")} 移到末尾`);
151
+ const actual = present.slice().sort((a, b) => idx(a) - idx(b));
152
+ if (actual.join("|") !== present.join("|")) out.push("force 顺序应为 " + FT.join(" → ") + "(最可能触发者最后)");
136
153
  return out;
137
154
  }
138
- /** 把 FORCE_TAIL 成员移到末尾(其余相对顺序不变);已正确时返回 null。 */
155
+ /** 把 forceTail 成员移到末尾(其余相对顺序不变);已正确时返回 null。 */
139
156
  export function normalizeForceOrder(packages: string[]): string[] | null {
140
- const present = FORCE_TAIL.filter((n) => packages.includes(n));
157
+ const FT = forceTail();
158
+ const present = FT.filter((n) => packages.includes(n));
141
159
  if (!present.length) return null;
142
- const rest = packages.filter((n) => !FORCE_TAIL.includes(n));
160
+ const rest = packages.filter((n) => !FT.includes(n));
143
161
  const next = [...rest, ...present];
144
162
  return next.join("\n") === packages.join("\n") ? null : next;
145
163
  }
@@ -158,7 +176,7 @@ function fixForceOrder(): string | null {
158
176
  s.packages = next;
159
177
  writeAtomic(SETTINGS, JSON.stringify(s, null, 2));
160
178
  } catch (e) { return `force 顺序自动修复失败:${(e as Error).message}`; }
161
- return `force 顺序已自动修复:${FORCE_TAIL.filter((n) => next.includes(n)).join(" / ")} 移到 packages 末尾`;
179
+ return `force 顺序已自动修复:${forceTail().filter((n) => next.includes(n)).join(" / ")} 移到 packages 末尾`;
162
180
  }
163
181
  /** 静态扫描:packages 中疑似 force 型扩展(入口源码含 return { systemPrompt } / forceSystemPrompt)。 */
164
182
  const FORCE_RE = /return\s*\{\s*systemPrompt|forceSystemPrompt/;
@@ -169,7 +187,7 @@ function classifyPackages(packages: string[]): { force: string[]; fallback: stri
169
187
  const force: string[] = [];
170
188
  const fallback: string[] = [];
171
189
  for (const name of packages) {
172
- if (!name.startsWith("npm:") || FORCE_TAIL.includes(name)) continue;
190
+ if (!name.startsWith("npm:") || forceTail().includes(name)) continue;
173
191
  const dir = path.join(HOME, ".pi", "agent", "npm", "node_modules", name.slice(4));
174
192
  if (!existsSync(dir)) continue;
175
193
  let entries: string[] = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-patch-keeper",
3
- "version": "1.2.11",
3
+ "version": "1.3.1",
4
4
  "description": "pi config guards: codemode single-source check, force-order guard (auto-fix), settings fingerprint drift, injected-manual alignment. (PiDeck / third-party source patches are a local-only module, not bundled.)",
5
5
  "type": "module",
6
6
  "license": "MIT",