@fanchao8609/agent_brain_sync 1.8.5 → 1.8.7
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/package.json +1 -1
- package/skill/abs-agent-brain-sync/SKILL.md +50 -5
- package/src/store.js +72 -12
- package/src/todo.js +61 -23
package/package.json
CHANGED
|
@@ -30,6 +30,7 @@ abs todo state <id> --note "进行中|讨论中|滞留中" # 改状态标记(
|
|
|
30
30
|
abs todo done <id> [--as 落地|否决|仅方案] # 完成(默认 落地)
|
|
31
31
|
abs log "完成 X:…" # 记一行工作成果(无参=查看)
|
|
32
32
|
abs note "经验一句话" [--tags 坑,docker] # 经验实时暂存 → sources/
|
|
33
|
+
abs concept <slug> --title "标题" [--tags a,b] # 建概念页骨架(头/中/尾,只给结构不给内容)
|
|
33
34
|
abs rule [add "一句话"] # 读写 index.md 的 ## Rules 硬规则
|
|
34
35
|
|
|
35
36
|
# 维护
|
|
@@ -127,7 +128,8 @@ load 只展示最新 5 条、每条按语义边界收口。
|
|
|
127
128
|
行形态:`- [ ] [进行中] <id> [[name]] — 说明 (认领 YYYY-MM-DD)`
|
|
128
129
|
|
|
129
130
|
`abs todo done <id>` 会勾选并归位到 Done 的日期组顶部,断点(`↳` 行)随迁;
|
|
130
|
-
Done 区由 `abs wrapup` 在会话结束时自动把「超 3
|
|
131
|
+
Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已完成」的组迁进
|
|
132
|
+
**当天会话快照的 `## 📦 任务归档` 段**(`sessions/log-<日期>.md`)—— 归档不另建文件
|
|
131
133
|
(任一天有未完成则整天不迁)。**什么时候动它见「进行中」一节。**
|
|
132
134
|
|
|
133
135
|
### `entities/` `concepts/` `sources/` `syntheses/` `sessions/`
|
|
@@ -138,10 +140,50 @@ Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已
|
|
|
138
140
|
| `concepts/` | 可复用的**规律/坑**(能说"这么做就避坑") | `docker-prisma-429.md` |
|
|
139
141
|
| `sources/` | 实时经验**暂存**(`abs note` 自动落) | `YYYY-MM-DD-slug.md` |
|
|
140
142
|
| `syntheses/` | **横向**选型/架构取舍(跨多个 entity/concept 的判断) | `synthesis-slug.md` |
|
|
141
|
-
| `sessions/` |
|
|
143
|
+
| `sessions/` | 会话快照(含 `## 🪝 Next Session Hook`)+ 当日任务归档段 | **`log-YYYY-MM-DD.md`** |
|
|
142
144
|
|
|
143
145
|
归类拿不准时**默认 `concepts/`**。
|
|
144
146
|
|
|
147
|
+
### `sessions/` 命名契约:一天一个文件(硬规则)
|
|
148
|
+
|
|
149
|
+
**一天只允许一个文件,且只能叫 `log-<日期>.md`。** 该日的一切都放进它:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
log-2026-09-08.md
|
|
153
|
+
├─ 会话快照正文(AI 手写:做了什么/怎么定位/结论)
|
|
154
|
+
├─ ## 关联连接
|
|
155
|
+
├─ ## 🪝 Next Session Hook(强制)
|
|
156
|
+
└─ ## 📦 任务归档(`abs todo archive` 自动写,机器生成勿手改)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**为什么定死**(实测 codebuddy 项目乱成这样,2026-09-15):同一天曾出现 **5 个文件**:
|
|
160
|
+
`2026-09-07-init-brain.md` / `-todo-md-archive.md` / `-todo归档.md` / `-ui-fixes.md` / `log-2026-09-07.md`,
|
|
161
|
+
且五份的 `tags` 各不相同(`source` / `source,archive` / `todo-archive` / `source,session-log` / `session-log`)。
|
|
162
|
+
同一天的记录散在多处,查一次要开五个文件。
|
|
163
|
+
|
|
164
|
+
**✅ 归档的正规做法**(用户 2026-09-15 定为标准,以后都这么干):
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
abs todo archive # 一条命令搞定,无需手工搬
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
它自动把过期日期组写进 `sessions/log-<那天>.md` 的 `## 📦 任务归档` 段:
|
|
171
|
+
- 该日**已有快照** → 只替换归档段,**段外的手写内容逐字保留**(不覆盖人工内容)
|
|
172
|
+
- 该日**没有快照** → 建一个只有归档段的 `log-<日期>.md`(那天本来就只有任务记录)
|
|
173
|
+
- 同日二次归档 → 段内追加,不重复建段
|
|
174
|
+
|
|
175
|
+
**结论:归档这件事不需要「方法」,只需要跑那条命令。**下面的禁令是给**手写**用的
|
|
176
|
+
(手写时才可能犯错)。
|
|
177
|
+
|
|
178
|
+
**四条禁令**(每条都有实测反例):
|
|
179
|
+
1. **不要**再把归档单独建文件(`<日期>-todo归档.md`)——归档已有专门的坑位段,`abs todo archive` 会写进去。
|
|
180
|
+
存量旧页不必手改(lint 不报),但新归档不再产生它。
|
|
181
|
+
2. **不要**在 `sessions/` 放 `tags: [source]` 的页——暂存页属 `sources/`。
|
|
182
|
+
当天做的一组工作不是「暂存线索」,而是**当天的快照正文**:直接写进 `log-<日期>.md`。
|
|
183
|
+
3. **不要**一天拆多个快照(`log-<日期>-<主题>.md`)——多主题就多写几个 `##` 段。
|
|
184
|
+
4. **不要**手工搬大文件进来当归档(如把仓库根 `todo.md` 全文倒进 `sessions/`)——
|
|
185
|
+
那要么进 `sources/`,要么摘出规律进 `concepts/`;全文属外部产物,用指针就行。
|
|
186
|
+
|
|
145
187
|
### 容量纪律(写任何页之前过四关)
|
|
146
188
|
|
|
147
189
|
图谱贵在**精**不在全,不过关就不写或压缩:
|
|
@@ -150,7 +192,7 @@ Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已
|
|
|
150
192
|
3. **sources 是暂存**:提炼成规律后删/归档,并清掉指向它的引用(防死链)。
|
|
151
193
|
4. **能不能用一行链接代替新增整页**?
|
|
152
194
|
|
|
153
|
-
`abs lint` 兜底:死链/孤岛/**悬挂页(NO-INBOUND)**/缺 frontmatter/超限/sources 堆积/**超龄未提炼(SOURCE-UNDISTILLED)**/index 漏列/Rules
|
|
195
|
+
`abs lint` 兜底:死链/孤岛/**悬挂页(NO-INBOUND)**/缺 frontmatter/超限/sources 堆积/**超龄未提炼(SOURCE-UNDISTILLED)**/index 漏列/Rules 超限/**缺尾(NO-TAIL,concept 页没有「做完怎么确认」)**。
|
|
154
196
|
## 开场:Init Sync(开工 / 默认续 todo)
|
|
155
197
|
|
|
156
198
|
图谱已存在;收到第一个核心开发指令**之前**走这条链载入上下文:
|
|
@@ -166,7 +208,7 @@ Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已
|
|
|
166
208
|
>
|
|
167
209
|
> **`## Rules` 区原样保留** —— 那是 load 要传达的状态本身(代码里明确不折)。
|
|
168
210
|
> 要全量明细:`abs todo --full` / `abs index`,或直接读 `.brain/` 文件、
|
|
169
|
-
> `.brain/sessions
|
|
211
|
+
> `.brain/sessions/log-<日期>.md`(含 `## 📦 任务归档` 段)。
|
|
170
212
|
2. **对账滞留(强制,别跳过)**:若 `abs load` 顶部出现 `⏳ 上会话滞留`,说明上会话有任务做完/做到一半就断了。**先收尾再开工**:
|
|
171
213
|
- 快照里的任务现在真做完了 → `abs todo done <id>`(done 后下次 load 滞留自动消失);
|
|
172
214
|
- 还没做完 → `abs todo note <id> --note "接到哪/改到哪个文件"` 补断点(别空手续接)。
|
|
@@ -253,7 +295,10 @@ Done 区由 `abs wrapup` 在会话结束时自动把「超 3 天且整天都已
|
|
|
253
295
|
任务告一段落/结束前,把**真实发生**写回图谱。只写做过/跑过/测过的事实,禁止脑补。按序:
|
|
254
296
|
|
|
255
297
|
1. **暂存线索**:`abs note`(或建 `sources/YYYY-MM-DD-slug.md`)记做了什么、改哪些文件、验证命令。
|
|
256
|
-
2. **抽规律**:值得留的 → `
|
|
298
|
+
2. **抽规律**:值得留的 → **用 `abs concept <slug> --title "…"` 建页**(自动带好四段骨架
|
|
299
|
+
并登记 index),再填内容。手写也行,但**四段位置别缺** —— 尤其末尾的「验证」段:
|
|
300
|
+
**没尾巴的经验只能被「相信」,不能被「验证」**(`abs lint` 会报 NO-TAIL)。
|
|
301
|
+
**判断仍归你**:这条值不值得留、该新建还是并入已有页 —— `abs concept` 只给结构,不替判断。
|
|
257
302
|
**同时检查:旧页里有没有被本次推翻的说法?** 有 → `abs supersede <旧页> --by <新页>`
|
|
258
303
|
(别删页;删了会让下个会话重踩同一个坑、重新记一遍)。核实过的新页把 `status` 改成 `active`
|
|
259
304
|
(`abs note` 落的页默认是 `draft`)。
|
package/src/store.js
CHANGED
|
@@ -4,7 +4,7 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import { join, resolve, dirname } from 'node:path';
|
|
5
5
|
import { requireBrain, brainPath, absLogDir, BRAIN_DIR } from './index.js';
|
|
6
6
|
import { requireUser, atTag, getUser } from './userconfig.js';
|
|
7
|
-
import { stripStateMark, ensureStateMark, normalizeTodo, addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText,
|
|
7
|
+
import { stripStateMark, ensureStateMark, normalizeTodo, addTask, upsertTask, boardText, readTodo, ensureTodo, todoTemplate, today, localStamp, setBreakpoint, setStateMark, TASK_STATES, insertDoneGrouped, idOfTaskLine, archiveDoneInText, upsertArchiveSection, DONE_KINDS, withDoneKind, doneKindOf, doneDateOf, collapseDone, SEC, rebuildStructure } from './todo.js';
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
10
10
|
|
|
@@ -685,16 +685,17 @@ export async function cmdTodoArchive({ dir, keepDays = 3, dryRun = false } = {})
|
|
|
685
685
|
return `[dry-run] 将归档 ${plan.archived.length} 天 / ${plan.count} 条(每天一个文件)\n ${brief}${why}`;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
-
// 1)
|
|
689
|
-
//
|
|
688
|
+
// 1) 归档目标 = 当天的会话快照文件(log-<日期>.md),写进其「任务归档」段。
|
|
689
|
+
// 为何合一(2026-09-15 用户定): 归档与会话快照是**同一天的记录**,分两个文件查着要开两处。
|
|
690
|
+
// 边界: ① 文件不存在→建只有归档段的页(那天可能没写快照)
|
|
691
|
+
// ② 文件已存在(含 AI 手写快照)→ 只替换归档段,段外逐字保留
|
|
692
|
+
// (硬规则「已存在的人工内容一律不覆盖」由 upsertArchiveSection 保证)。
|
|
690
693
|
const sessDir = brainPath(root, 'sessions');
|
|
691
694
|
for (const g of plan.archived) {
|
|
692
695
|
const pageP = join(sessDir, `${g.slug}.md`);
|
|
693
696
|
let page = null;
|
|
694
697
|
try { page = await fs.readFile(pageP, 'utf8'); } catch { /* 首次 */ }
|
|
695
|
-
const nextPage = page
|
|
696
|
-
? renderArchivePage({ group: g })
|
|
697
|
-
: page.replace(/\s*$/, '') + '\n\n' + renderArchiveBody([g]) + '\n';
|
|
698
|
+
const nextPage = upsertArchiveSection(page, g);
|
|
698
699
|
const tmp = join(sessDir, `.${g.slug}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
|
|
699
700
|
await fs.writeFile(tmp, nextPage, 'utf8');
|
|
700
701
|
await fs.rename(tmp, pageP);
|
|
@@ -707,15 +708,17 @@ export async function cmdTodoArchive({ dir, keepDays = 3, dryRun = false } = {})
|
|
|
707
708
|
});
|
|
708
709
|
|
|
709
710
|
// 3) index 登记(每个日期页一行,幂等)
|
|
711
|
+
// 坑(2026-09-15 修): 原先把归档页登记到 `## Sources` 区 —— 但它们在 sessions/ 里,
|
|
712
|
+
// 旧数据实测就是错位的(09-10/09-12 两条曾键在 Sources 段,人工才发现)。现改登 `## Sessions`。
|
|
710
713
|
const iP = brainPath(root, 'index.md');
|
|
711
714
|
await editFile(iP, (index) => {
|
|
712
715
|
if (!index) return SKIP;
|
|
713
716
|
const missing = plan.archived.filter((g) => !index.includes(`[[${g.slug}]]`));
|
|
714
717
|
if (!missing.length) return SKIP;
|
|
715
|
-
const sIdx = index.indexOf('##
|
|
718
|
+
const sIdx = index.indexOf('## Sessions');
|
|
716
719
|
if (sIdx === -1) return SKIP;
|
|
717
720
|
const after = index.indexOf('\n## ', sIdx + 1);
|
|
718
|
-
const add = missing.map((g) => `- [[${g.slug}]] —
|
|
721
|
+
const add = missing.map((g) => `- [[${g.slug}]] — ${g.date}(会话快照 + 任务归档)`).join('\n');
|
|
719
722
|
const next = after === -1
|
|
720
723
|
? `${index.replace(/\s*$/, '')}\n${add}\n`
|
|
721
724
|
: index.slice(0, after) + `\n${add}` + index.slice(after);
|
|
@@ -1355,9 +1358,13 @@ export async function cmdLint({ dir }) {
|
|
|
1355
1358
|
issues.push(`DEAD-LINK: ${pg.rel} -> [[${ln}]]`);
|
|
1356
1359
|
}
|
|
1357
1360
|
}
|
|
1358
|
-
// ORPHAN: sources/
|
|
1359
|
-
//
|
|
1360
|
-
|
|
1361
|
+
// ORPHAN: sources/ 暂存页与会话/归档页豁免。前者是暂存线索(提炼成 concept 前天然孤立),
|
|
1362
|
+
// 后者是历史记录(已登记在 index.md,就是图谱入口,无需再制造双链)。
|
|
1363
|
+
// 豁免名单含两种归档命名:旧 `*-todo归档`(存量页仍在)+ 新 `log-*`
|
|
1364
|
+
//(2026-09-15 起归档并入当天快照,见 todo.js 的 upsertArchiveSection)。
|
|
1365
|
+
const isTerminal = pg.dir === 'sources'
|
|
1366
|
+
|| /todo归档$/.test(pg.slug)
|
|
1367
|
+
|| (pg.dir === 'sessions' && /^log-/.test(pg.slug));
|
|
1361
1368
|
if (!isTerminal && !pg.links.length && !linkedNames.has(pg.slug)) {
|
|
1362
1369
|
issues.push(`ORPHAN-PAGE: ${pg.rel} (no links out, no links in)`);
|
|
1363
1370
|
}
|
|
@@ -1367,7 +1374,12 @@ export async function cmdLint({ dir }) {
|
|
|
1367
1374
|
if (['concepts', 'entities', 'syntheses'].includes(pg.dir) && !(inbound.get(pg.slug) || 0)) {
|
|
1368
1375
|
issues.push(`NO-INBOUND: ${pg.rel} (无人链接到本页;在相关页的 ## 关联连接 挂一条 [[${pg.slug}]])`);
|
|
1369
1376
|
}
|
|
1370
|
-
|
|
1377
|
+
// UNRESOLVED-CONFLICT: 有「## 知识冲突」段但还是 draft = 冲突标了没裁决。
|
|
1378
|
+
// 判据必须认【段标题】而非页内出现「知识冲突」字样。
|
|
1379
|
+
// 坑(2026-09-15 实测): 原用裸子串 → codebuddy 的会话快照因任务描述里写了
|
|
1380
|
+
// 「更新 session-key-fingerprint-flaw(知识冲突裁决)」而误报(它并无该段)。
|
|
1381
|
+
// 同 NO-TAIL 的教训: 判据看结构,不看关键词。
|
|
1382
|
+
if (/^#{2,6}[^\n]*知识冲突/m.test(pg.body) && /status: draft/.test(pg.frontmatter)) {
|
|
1371
1383
|
issues.push(`UNRESOLVED-CONFLICT: ${pg.rel}`);
|
|
1372
1384
|
}
|
|
1373
1385
|
if (['concepts', 'entities', 'syntheses'].includes(pg.dir)) {
|
|
@@ -1419,6 +1431,54 @@ export async function cmdLint({ dir }) {
|
|
|
1419
1431
|
}
|
|
1420
1432
|
}
|
|
1421
1433
|
|
|
1434
|
+
// SESSIONS-NAMING: sessions/ 的一天一文件契约(见 skill 的「sessions/ 命名契约」)。
|
|
1435
|
+
// 实测 codebuddy 乱局(2026-09-15):一天最多出现 5 个文件、5 种 tags、同天两个快照。
|
|
1436
|
+
// 判据纯机械:按“日期前缀”归组——同天 >1 个文件报 SPLIT;单个但非规范名报 NAMING。
|
|
1437
|
+
// 只看文件名,不猜内容。
|
|
1438
|
+
const sessByDate = new Map();
|
|
1439
|
+
for (const pg of pages) {
|
|
1440
|
+
if (pg.dir !== 'sessions') continue;
|
|
1441
|
+
// 同时认两种写法:规范名 `log-<日期>` 与旧/杂命名 `<日期>-…`。
|
|
1442
|
+
// 坑(写测试时抓到的): 首版只写 `^(\d{4}-…)` → **匹配不上规范名 `log-2026-09-07`**,
|
|
1443
|
+
// 于是「一个 log- + 一个旧杂文件」被数成 1 个而非 2 个,漏报。
|
|
1444
|
+
const m = pg.slug.match(/^(?:log-)?(\d{4}-\d{2}-\d{2})/);
|
|
1445
|
+
if (!m) continue;
|
|
1446
|
+
// 长期存续的归档页豁免:**只认独立的 `archive` 标签**(如 `tags: [session-log, archive]`),
|
|
1447
|
+
// 不认 `todo-archive`(那是旧归档页的标签,它正是要迁移的对象)。
|
|
1448
|
+
// 坑(2026-09-15 在 ~/Docker 实测抓到): 首版用 `\barchive\b` —— 而 `todo-archive`
|
|
1449
|
+
// 里 `-` 与 `a` 之间也是词边界 → **老式归档页全被豁免**,一个都不报(漏报四天)。
|
|
1450
|
+
// 豁免是为「外部产物全文归档」(如仓库 todo.md 全文)设的,不是为旧命名归档页。
|
|
1451
|
+
const tags = String(pg.frontmatter).match(/^tags:\s*(.+)$/m)?.[1] || '';
|
|
1452
|
+
const tagList = tags.replace(/^\[|\]$/g, '').split(',').map((t) => t.trim());
|
|
1453
|
+
if (tagList.includes('archive')) continue;
|
|
1454
|
+
if (!sessByDate.has(m[1])) sessByDate.set(m[1], []);
|
|
1455
|
+
sessByDate.get(m[1]).push(pg.slug);
|
|
1456
|
+
}
|
|
1457
|
+
for (const [date, slugs] of sessByDate) {
|
|
1458
|
+
if (slugs.length > 1) {
|
|
1459
|
+
issues.push(`SESSIONS-SPLIT: ${date} 在 sessions/ 有 ${slugs.length} 个文件(${slugs.join('、')});` +
|
|
1460
|
+
`一天只应有一个 \`log-${date}.md\`:归档写进其「## 📦 任务归档」段,多主题写成多个 ## 子段`);
|
|
1461
|
+
} else if (!slugs[0].startsWith('log-')) {
|
|
1462
|
+
// 单个文件但**不是规范名** —— 旧命名(`<日期>-todo归档.md` 等)单独存留。
|
|
1463
|
+
// 坑(2026-09-15 在 ~/Docker 实测抓到): 首版只看“同天 >1 个” → 4 个日期
|
|
1464
|
+
// 各只有一份 `<日期>-todo归档.md` → 一个都不报(漏报)。
|
|
1465
|
+
// 旧命名的页无论是否孤单都该改:跑 `abs todo archive` 后并入 `log-<日期>.md`。
|
|
1466
|
+
issues.push(`SESSIONS-NAMING: ${date} 的文件 \`${slugs[0]}.md\` 不是规范名;` +
|
|
1467
|
+
`应为 \`log-${date}.md\`(跑 abs todo archive 会并入;旧归档页可删)`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
// SESSIONS-MISPLACED: sessions/ 里放了 tags 既非 session-log / todo-archive / archive 的页。
|
|
1471
|
+
// 实例:codebuddy 的 `2026-09-07-ui-fixes.md`(tags: [source,session-log])等 5 页 ——
|
|
1472
|
+
// 当时做的一组工作不是「暂存线索」,而就是当天的快照正文。
|
|
1473
|
+
for (const pg of pages) {
|
|
1474
|
+
if (pg.dir !== 'sessions') continue;
|
|
1475
|
+
const tags = String(pg.frontmatter).match(/^tags:\s*(.+)$/m)?.[1] || '';
|
|
1476
|
+
if (!/session-log|todo-archive|archive/.test(tags)) {
|
|
1477
|
+
issues.push(`SESSIONS-MISPLACED: ${pg.rel}(tags: ${tags.trim()} 不属 sessions/;` +
|
|
1478
|
+
`当天工作写进 \`log-<日期>.md\` 正文,暂存线索用 \`abs note\` 落 sources/)`);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1422
1482
|
// SUPERSEDED-DANGLING: superseded 页声明的取代者也必须存在。
|
|
1423
1483
|
// 它跟 DEAD-LINK 同性质(指向不存在的页),但后果更重:
|
|
1424
1484
|
// 读者被引导去找一个不存在的"新版本",比单纯断链更容易让人以为"没新页就是没替代"。
|
package/src/todo.js
CHANGED
|
@@ -426,13 +426,13 @@ function isUndoneLine(l) {
|
|
|
426
426
|
* 规则(用户定):
|
|
427
427
|
* ① 只保留近 keepDays 天(含今天);更早的才归档。
|
|
428
428
|
* ② 某一天只要还有未完成(- [ ])任务,**整天都不归档**(不拆半天)。
|
|
429
|
-
* ③
|
|
430
|
-
*
|
|
431
|
-
* `- [[
|
|
429
|
+
* ③ 每**天**一个文件,slug 为 `log-<日期>`(由 slugFor 给)—— 即**归进那天的会话快照**
|
|
430
|
+
* (同一天的东西放一处,别为归档另建文件)。本函数只负责从 todo 文本里移除
|
|
431
|
+
* + 在 Done 区尾部的 `### 归档` 区**每天记一行** `- [[log-<日期>]] 完成任务 N 条`。
|
|
432
432
|
* 无日期组(### (未标日期))无法判天数,**保守不归档**。
|
|
433
433
|
* @returns {{text:string, archived:{date:string,lines:string[],slug:string,count:number}[], skipped:{date:string,reason:string}[], count:number}}
|
|
434
434
|
*/
|
|
435
|
-
export function archiveDoneInText(text, { keepDays = 3, from = today(), slugFor = (d) =>
|
|
435
|
+
export function archiveDoneInText(text, { keepDays = 3, from = today(), slugFor = (d) => `log-${d}` } = {}) {
|
|
436
436
|
const lines = String(text || '').split('\n');
|
|
437
437
|
const di = lines.findIndex((l) => l.startsWith('## Done'));
|
|
438
438
|
if (di === -1) return { text, archived: [], skipped: [], count: 0 };
|
|
@@ -484,31 +484,69 @@ export function archiveDoneInText(text, { keepDays = 3, from = today(), slugFor
|
|
|
484
484
|
return { text: rebuilt.join('\n').replace(/\n+$/, '\n'), archived, skipped, count };
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
-
/**
|
|
487
|
+
/** 归档页正文(按日期分组,原文保留)。供同日追写复用。 */
|
|
488
488
|
export function renderArchiveBody(groups) {
|
|
489
489
|
const out = [];
|
|
490
490
|
for (const g of groups) out.push(`### ${g.date}`, '', ...g.lines, '');
|
|
491
491
|
return out.join('\n').replace(/\n+$/, '\n');
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
/**
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
494
|
+
/** 归档区标题。归档内容全部落在这个二级标题下,**与 AI 手写的会话快照共存**:
|
|
495
|
+
* 同一天的记录(快照 + 任务明细)放一个文件里,而不是分两个文件。
|
|
496
|
+
* 写入侧只动这一段:标题之前的内容原样保留(已存在的人工内容一律不覆盖)。 */
|
|
497
|
+
export const ARCHIVE_SECTION = '## 📦 任务归档';
|
|
498
|
+
|
|
499
|
+
/** 把「任务归档」段合并进已有正文。**纯函数**(不碰磁盘)。
|
|
500
|
+
*
|
|
501
|
+
* 为何不再「每天一个文件」(2026-09-15 用户定):
|
|
502
|
+
* 归档页与 `log-<日期>.md` 会话快照是**同一天的记录**,分两处查着要开两个文件。
|
|
503
|
+
* 改为:归档写进当天快照的 `ARCHIVE_SECTION` 段。
|
|
504
|
+
*
|
|
505
|
+
* 两个边界(都有测试钉住):
|
|
506
|
+
* ① 文件不存在 → 建一个**只有归档段**的页(那天可能没写快照,仍只落这一个文件)。
|
|
507
|
+
* ② 文件已存在(含 AI 手写的快照)→ **只替换归档段**,段外内容逐字保留。
|
|
508
|
+
* 同日重复归档(罕见)则把新日期组接在段内已有内容之后,不重复建段。
|
|
509
|
+
*
|
|
510
|
+
* @param body 现有全文(null = 文件不存在)
|
|
511
|
+
* @param group { date, lines, count }
|
|
512
|
+
*/
|
|
513
|
+
export function upsertArchiveSection(body, group) {
|
|
514
|
+
const chunk = renderArchiveBody([group]);
|
|
515
|
+
if (body == null) {
|
|
516
|
+
// ① 新文件:只有归档段(无手写快照也合法 —— 那天本来就只有任务记录)
|
|
517
|
+
const fm = [
|
|
518
|
+
'---',
|
|
519
|
+
'tags: [session-log, todo-archive, 历史]',
|
|
520
|
+
`updated: ${group.date}`,
|
|
521
|
+
'status: reviewed',
|
|
522
|
+
'---',
|
|
523
|
+
'',
|
|
524
|
+
`# ${group.date} 记录`,
|
|
525
|
+
'',
|
|
526
|
+
'> 本页 = 该日的会话快照(若有)+ 从 `todo.md` Done 区迁出的任务明细。',
|
|
527
|
+
'',
|
|
528
|
+
ARCHIVE_SECTION,
|
|
529
|
+
'',
|
|
530
|
+
chunk.trimEnd(),
|
|
531
|
+
'',
|
|
532
|
+
];
|
|
533
|
+
return fm.join('\n');
|
|
534
|
+
}
|
|
535
|
+
// ② 已存在:只动归档段,段外原样
|
|
536
|
+
const lines = body.split('\n');
|
|
537
|
+
const i = lines.findIndex((l) => l.trim() === ARCHIVE_SECTION);
|
|
538
|
+
if (i === -1) {
|
|
539
|
+
// 有文件但还没归档段 → 追加到末尾(不搅动已有内容)
|
|
540
|
+
return `${body.replace(/\s*$/, '')}\n\n${ARCHIVE_SECTION}\n\n${chunk.trimEnd()}\n`;
|
|
541
|
+
}
|
|
542
|
+
// 找到本段结束(下一个同级或更高级标题)
|
|
543
|
+
let j = i + 1;
|
|
544
|
+
while (j < lines.length && !/^#{1,2} \S/.test(lines[j].trim())) j++;
|
|
545
|
+
const head = lines.slice(0, i + 1);
|
|
546
|
+
const tailPart = lines.slice(j);
|
|
547
|
+
const existingInner = lines.slice(i + 1, j).join('\n').trim();
|
|
548
|
+
const merged = existingInner ? `${existingInner}\n\n${chunk.trimEnd()}` : chunk.trimEnd();
|
|
549
|
+
return [...head, '', merged, '', ...tailPart].join('\n').replace(/\n{3,}/g, '\n\n');
|
|
512
550
|
}
|
|
513
551
|
|
|
514
552
|
/** 幂等:把 todo 全文里平铺的旧 Done 区按日期分组(新日期在前,未标日期归尾)。
|