add-coder 0.3.37 → 0.3.39

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.
@@ -0,0 +1,270 @@
1
+ /*
2
+ * Plan ↔ 兄弟制品(add-route / handoff / review)版本配对解析 —— 单一真源
3
+ *
4
+ * 动因(2026-09-18,farm-agent 多 v2 Plan 暴露):
5
+ * 旧实现按 planKeyword **去掉版本后缀取首个匹配** → 命中日期最早的 v1 add-route;
6
+ * 且 Plan 解析只要求文件名含 "-plan-v" → `...-plan-v2.hitl.md` 提案文件会顶替真正的 Plan。
7
+ * 后果:check_spec_sync 拿旧版路线图的附录比对当前 git diff,报出几十条「未登记」假告警;
8
+ * check_dps / check_add_route_status / check_add_route_completeness 同样读错文档。
9
+ * 同类问题第三例:git diff 路径被引号+八进制转义(`".codex/plans/...\346\236..."`),
10
+ * 既躲过 magic 前缀豁免、也与附录里的真实中文路径比不中 → 统一走 `-z`(NUL 分隔,永不加引号)。
11
+ *
12
+ * 解析优先级(可否证伪、不静默降级):
13
+ * Plan : 关键词命中且 `-plan-vN.md` **结尾**(`.hitl.md`/devlog/handoff/add-route/review 天然排除)
14
+ * → 版本号最大;同版本按路径降序(日期新者优先)
15
+ * 兜底:关键词命中、含 "plan"、非兄弟产物的 `.md`
16
+ * add-route : ① 同目录 + 同基名 + 同版本 ② 同基名最高版本(同版本优先 Plan 所在目录)
17
+ * ③ 关键词命中最高版本(基名与 Plan 不同时的兼容路径)
18
+ * 版本落后(add-route vN < Plan vM)→ 显式 warning,不静默
19
+ */
20
+
21
+ export type ArtifactKind = "plan" | "add-route" | "handoff" | "review";
22
+
23
+ export type ResolveVia =
24
+ | "plan-max-version"
25
+ | "plan-fallback"
26
+ | "paired-same-dir-version"
27
+ | "paired-base-max-version"
28
+ | "keyword-max-version";
29
+
30
+ export interface ResolvedArtifact {
31
+ /** 相对 plans 目录的路径(POSIX 分隔符) */
32
+ file: string;
33
+ kind: ArtifactKind;
34
+ /** 文件名解析出的版本号;无 `-vN` 后缀 = 0 */
35
+ version: number;
36
+ /** 去掉 `-<kind>-vN.md` 后的基名(review 的 `-review-implementation` 变体一并剥离) */
37
+ base: string;
38
+ /** 所在目录(相对,POSIX;根目录 = "") */
39
+ dir: string;
40
+ /** 解析依据(可审计,便于区分「配对命中」与「关键词兜底」) */
41
+ via: ResolveVia;
42
+ /** 与 Plan 版本不匹配时的显式告警(null = 无) */
43
+ warning: string | null;
44
+ }
45
+
46
+ export interface ResolveResult {
47
+ plan: ResolvedArtifact | null;
48
+ /** kind 非 plan 时:与已解析 Plan 配对的制品;解析不到 = null */
49
+ artifact: ResolvedArtifact | null;
50
+ }
51
+
52
+ const KIND_SUFFIX: Record<ArtifactKind, RegExp> = {
53
+ plan: /-plan-v(\d+)\.md$/i,
54
+ "add-route": /-add-route-v(\d+)\.md$/i,
55
+ handoff: /-handoff-v(\d+)\.md$/i,
56
+ review: /-review(?:-[a-z0-9-]+)?-v(\d+)\.md$/i,
57
+ };
58
+
59
+ /** Plan 解析必须排除的兄弟产物(提案/日志/契约/下游文档) */
60
+ const NON_PLAN_SUFFIX =
61
+ /(\.hitl\.md$)|(-add-route-v\d+\.md$)|(-handoff-v\d+\.md$)|(-review(?:-[a-z0-9-]+)?-v\d+\.md$)|(-contract-v\d+\.md$)|(^devlog-)/i;
62
+
63
+ export function toPosix(p: string): string {
64
+ return p.replace(/\\/g, "/");
65
+ }
66
+
67
+ function baseOf(p: string): string {
68
+ const posix = toPosix(p);
69
+ const i = posix.lastIndexOf("/");
70
+ return i < 0 ? posix : posix.slice(i + 1);
71
+ }
72
+
73
+ function dirOf(p: string): string {
74
+ const posix = toPosix(p);
75
+ const i = posix.lastIndexOf("/");
76
+ return i < 0 ? "" : posix.slice(0, i);
77
+ }
78
+
79
+ /** 路径降序(日期新者优先,确定性;避免依赖 readdir 顺序) */
80
+ function byPathDesc(a: string, b: string): number {
81
+ return a < b ? 1 : a > b ? -1 : 0;
82
+ }
83
+
84
+ export function parseArtifactName(
85
+ name: string,
86
+ kind: ArtifactKind,
87
+ ): { base: string; version: number } | null {
88
+ const base = baseOf(name);
89
+ const m = base.match(KIND_SUFFIX[kind]);
90
+ if (!m || m.index === undefined) return null;
91
+ return { base: base.slice(0, m.index), version: parseInt(m[1], 10) };
92
+ }
93
+
94
+ /**
95
+ * `git ... --name-only -z` 输出 → 路径数组。
96
+ * `-z` 下 git **永不**加引号/转义(非 ASCII 路径保持原样),因此这里只需切分。
97
+ * 无 NUL 时按行切分(兼容不支持的旧 git),并清掉 CR。
98
+ */
99
+ export function splitGitPathList(stdout: string): string[] {
100
+ const parts = stdout.includes("\u0000") ? stdout.split("\u0000") : stdout.split("\n");
101
+ return parts
102
+ .map((p) => p.replace(/\r$/, ""))
103
+ .filter((p) => p !== "")
104
+ .map(toPosix);
105
+ }
106
+
107
+ /** add-route / review 附录里登记的文件清单(反引号内的路径) */
108
+ export function extractAppendixFiles(content: string): string[] {
109
+ return (
110
+ content.match(/`[^`]+\.(ts|tsx|js|jsx|sh|sql|prisma|md|json|yml|yaml|toml)`/g) || []
111
+ ).map((f) => f.replace(/`/g, ""));
112
+ }
113
+
114
+ function build(
115
+ file: string,
116
+ kind: ArtifactKind,
117
+ parsed: { base: string; version: number },
118
+ via: ResolveVia,
119
+ warning: string | null = null,
120
+ ): ResolvedArtifact {
121
+ return {
122
+ file: toPosix(file),
123
+ kind,
124
+ version: parsed.version,
125
+ base: parsed.base,
126
+ dir: dirOf(file),
127
+ via,
128
+ warning,
129
+ };
130
+ }
131
+
132
+ /** 解析 Plan 文件:版本最高者优先;`.hitl.md` 等兄弟产物不入选 */
133
+ export function resolvePlan(
134
+ files: readonly string[],
135
+ keyword: string,
136
+ ): ResolvedArtifact | null {
137
+ const kw = keyword.trim().toLowerCase();
138
+ if (!kw) return null;
139
+ const pool = files.map(toPosix).filter((f) => f.toLowerCase().includes(kw));
140
+
141
+ const versioned = pool
142
+ .map((f) => ({ f, parsed: parseArtifactName(f, "plan") }))
143
+ .filter(
144
+ (e): e is { f: string; parsed: { base: string; version: number } } =>
145
+ e.parsed !== null && !NON_PLAN_SUFFIX.test(baseOf(e.f)),
146
+ )
147
+ .sort((a, b) => b.parsed.version - a.parsed.version || byPathDesc(a.f, b.f));
148
+ if (versioned.length > 0) {
149
+ const { f, parsed } = versioned[0];
150
+ return build(f, "plan", parsed, "plan-max-version");
151
+ }
152
+
153
+ // 兜底:无 `-plan-vN` 后缀的 Plan(精简版/历史命名)——仍需排除兄弟产物
154
+ const fallback = pool
155
+ .filter(
156
+ (f) =>
157
+ f.toLowerCase().endsWith(".md") &&
158
+ /plan/i.test(baseOf(f)) &&
159
+ !NON_PLAN_SUFFIX.test(baseOf(f)),
160
+ )
161
+ .sort(byPathDesc);
162
+ if (fallback.length > 0) {
163
+ return build(fallback[0], "plan", { base: baseOf(fallback[0]).replace(/\.md$/i, ""), version: 0 }, "plan-fallback");
164
+ }
165
+ return null;
166
+ }
167
+
168
+ /** 与 Plan 配对的制品:同目录同版本 → 同基名最高版本 → 关键词最高版本 */
169
+ export function resolvePairedArtifact(
170
+ files: readonly string[],
171
+ plan: ResolvedArtifact,
172
+ kind: ArtifactKind,
173
+ keyword: string,
174
+ ): ResolvedArtifact | null {
175
+ const posix = files.map(toPosix);
176
+ const suffix = `-${kind}-v${plan.version}.md`;
177
+ const expected = plan.dir ? `${plan.dir}/${plan.base}${suffix}` : `${plan.base}${suffix}`;
178
+ const exact = posix.find((f) => f.toLowerCase() === expected.toLowerCase());
179
+ if (exact) return build(exact, kind, { base: plan.base, version: plan.version }, "paired-same-dir-version");
180
+
181
+ const sameDirScore = (f: string) => (dirOf(f) === plan.dir ? 0 : 1);
182
+ const sameBase = posix
183
+ .map((f) => ({ f, parsed: parseArtifactName(f, kind) }))
184
+ .filter(
185
+ (e): e is { f: string; parsed: { base: string; version: number } } =>
186
+ e.parsed !== null && e.parsed.base.toLowerCase() === plan.base.toLowerCase(),
187
+ )
188
+ .sort(
189
+ (a, b) =>
190
+ b.parsed.version - a.parsed.version ||
191
+ sameDirScore(a.f) - sameDirScore(b.f) ||
192
+ byPathDesc(a.f, b.f),
193
+ );
194
+ if (sameBase.length > 0) {
195
+ const { f, parsed } = sameBase[0];
196
+ return build(f, kind, parsed, "paired-base-max-version", versionWarning(plan, parsed.version, kind));
197
+ }
198
+
199
+ const kw = keyword.trim().toLowerCase();
200
+ const byKeyword = posix
201
+ .filter((f) => f.toLowerCase().includes(kw))
202
+ .map((f) => ({ f, parsed: parseArtifactName(f, kind) }))
203
+ .filter(
204
+ (e): e is { f: string; parsed: { base: string; version: number } } => e.parsed !== null,
205
+ )
206
+ .sort((a, b) => b.parsed.version - a.parsed.version || byPathDesc(a.f, b.f));
207
+ if (byKeyword.length > 0) {
208
+ const { f, parsed } = byKeyword[0];
209
+ const warn = versionWarning(plan, parsed.version, kind);
210
+ const base = warn
211
+ ? warn
212
+ : `基名不同:Plan 基名 "${plan.base}",${kind} 基名 "${parsed.base}"(按关键词兜底命中,请确认对应关系)`;
213
+ return build(f, kind, parsed, "keyword-max-version", base);
214
+ }
215
+ return null;
216
+ }
217
+
218
+ function versionWarning(
219
+ plan: ResolvedArtifact,
220
+ artifactVersion: number,
221
+ kind: ArtifactKind,
222
+ ): string | null {
223
+ if (plan.version > 0 && artifactVersion < plan.version) {
224
+ return `${kind} v${artifactVersion} 落后于 Plan v${plan.version}(Plan 基名 "${plan.base}")——补齐对应版本,或确认沿用旧版路线图`;
225
+ }
226
+ return null;
227
+ }
228
+
229
+ /** 工具入口:一次拿到「Plan + 配对制品」 */
230
+ export function resolvePlanArtifact(
231
+ files: readonly string[],
232
+ keyword: string,
233
+ kind: ArtifactKind,
234
+ ): ResolveResult {
235
+ const plan = resolvePlan(files, keyword);
236
+ if (!plan) return { plan: null, artifact: null };
237
+ if (kind === "plan") return { plan, artifact: plan };
238
+ return { plan, artifact: resolvePairedArtifact(files, plan, kind, keyword) };
239
+ }
240
+
241
+ /**
242
+ * 未登记文件的归属分摊:逐个读其它 add-route 的附录,命中即记为「属于该 Plan」。
243
+ * 全部命中即提前返回(不做无谓的全量读取)。顺序由调用方给定(建议新者优先)。
244
+ */
245
+ export async function attributeToOtherPlans(
246
+ unmatched: readonly string[],
247
+ otherRoutes: readonly string[],
248
+ readRoute: (rel: string) => Promise<string | null>,
249
+ ): Promise<{
250
+ /** key = 小写路径(比对用),value = 原名 + 归属 add-route */
251
+ owners: Map<string, { file: string; route: string }>;
252
+ /** 任何 add-route 都未登记的变更文件(原名) */
253
+ unowned: string[];
254
+ }> {
255
+ const owners = new Map<string, { file: string; route: string }>();
256
+ let remaining = unmatched.map((f) => ({ key: f.toLowerCase(), file: f }));
257
+ for (const route of otherRoutes) {
258
+ if (remaining.length === 0) break;
259
+ const content = await readRoute(route);
260
+ if (!content) continue;
261
+ const appendix = new Set(extractAppendixFiles(content).map((f) => f.toLowerCase()));
262
+ const stillRemaining: Array<{ key: string; file: string }> = [];
263
+ for (const e of remaining) {
264
+ if (appendix.has(e.key)) owners.set(e.key, { file: e.file, route });
265
+ else stillRemaining.push(e);
266
+ }
267
+ remaining = stillRemaining;
268
+ }
269
+ return { owners, unowned: remaining.map((e) => e.file) };
270
+ }
@@ -4,6 +4,7 @@
4
4
  > - `[T]` = 编译期验证—证据: 命令+结果(如 `tsc=0` / `vitest 18/18`)
5
5
  > - `[R]` = 运行时验证—证据: 部署后确认(如 `curl 200`)
6
6
  > - `[E]` = 静态检查—证据: grep/diff 输出
7
+ > - `[W]` = **接线验证(Wiring)**—证据: 生产调用点存在的 grep / 端到端一次真实产出(**实现存在 ≠ 生产可达**)
7
8
  >
8
9
  > **审计链(证据→devlog→checklist)**:
9
10
  > - 初验规则: 先找证据(命令+结果)→ 调 `record_dev_operation` 落库 → 将返回的真实 cuid(25位)写入 checklist。**禁止抄写 `cmq...` 占位符**。
@@ -14,6 +15,7 @@
14
15
  - [ ] [T] 检查项1 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
15
16
  - [ ] [T] 检查项2 — 证据: (待填写)|审计: (初验从 record_dev_operation 返回值写入真实 cuid,**禁止写 `cmq...` 占位符**)
16
17
  - [ ] [T] schema 变更后运行实例 client 新鲜度(`db:generate` + 重启校验)— 证据: (待填写)|审计: (待填写) [来源: farm-agent 9737ab4 回灌;Prisma+Atlas 项目通用]
18
+ - [ ] [W] 新增的"被调用方"(策略/服务/工具/证据产生者)存在**生产调用点** — 证据: `grep -rn "<symbol>" src/ --include=*.ts` 有**非测试**命中且位于真实执行路径(非仅导出)| 审计: (待填写) [来源: farm-agent 2026-09-18 回流 P0 #R9;同类三例均为「纯函数+单测齐全、生产不可达」]
17
19
 
18
20
  ## ADD 规则合规检查
19
21
 
@@ -98,6 +98,24 @@
98
98
 
99
99
  ---
100
100
 
101
+ ## 6.2 接线可达性核对(`[W]` 接线判据 · 2026-09-18 增补)
102
+
103
+ > **动因**:同类事故已发生**三次**(回流源:压缩通道 / 补向量 / 沉淀链),三次都是"**纯函数与单测齐全、生产不可达**",
104
+ > 而三项漂移指纹(范围扩散 / 类型错误 / 审计漏记)**全绿**——它们测的是"改得规不规矩",不是"能力是否可达"。
105
+ > 本节的判据是**可否证伪的 grep 与端到端产出**;"实现了 / 有单测 / 已导出"**不作为通过理由**。
106
+ > [来源: farm-agent 2026-09-18 回流 P0 #R9;域符号名已泛化为占位符]
107
+
108
+ 对本次变更涉及的每个"被调用方"(策略 / 服务 / 工具 / 证据产生者 / 审计字面量)逐项核对:
109
+
110
+ | # | 被调用方(symbol) | 期望调用点 | grep 证据(非测试命中) | 端到端产出证据 | 判定 |
111
+ |---|-------------------|-----------|------------------------|---------------|:----:|
112
+ | 1 | {symbol} | {文件:行} | `grep -rn "<symbol>" src/ --include=*.ts` 命中数 = {N} | {DB 行数 / SSE 帧 / 审计记录} | ✅/❌ |
113
+
114
+ - [ ] 每个新增 symbol 有**非测试**调用点(**仅导出 / 仅被测试 import = 未接线 ⇒ 判 ❌**)
115
+ - [ ] 涉及审计字面量时:该字面量在本次运行时**至少被写出过一次**(`query_audit_logs` 可查,而非"代码里有这行")
116
+ - [ ] 端到端产出可证(落库行数 / 帧 / 文件产物),而非"单测断言通过"
117
+ - [ ] 本节表格已填;若本次变更**不含**新增被调用方,须显式写"本变更无新增被调用方",不得静默跳过
118
+
101
119
  ## 7. 关联 Checklist
102
120
 
103
121
  - 本 review 的检查项与 `{checklist 文档路径}` 的"跨项目联调检查"章节一一对应
@@ -42,6 +42,12 @@
42
42
  "heading": "## 6. E2E 逐端点 curl",
43
43
  "required": true
44
44
  },
45
+ {
46
+ "id": "wiring",
47
+ "heading": "## 6.2 接线可达性核对",
48
+ "required": true,
49
+ "note": "2026-09-18 增补(回流 farm-agent P0 #R9):逐项核对新增被调用方的生产调用点与端到端产出([W] 判据)。三次同类事故(纯函数与单测齐全、生产不可达)均在此节可被拦截。"
50
+ },
45
51
  {
46
52
  "id": "checklist",
47
53
  "heading": "## 7. 关联 Checklist",
@@ -61,4 +67,4 @@
61
67
  "阶段",
62
68
  "步骤"
63
69
  ]
64
- }
70
+ }