@fanchao8609/agent_brain_sync 1.19.6 → 1.20.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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { TODO_SECTIONS, H1_TO_FILE, TASK_STATES, RETIRED_SECTIONS } from './spec.js';
4
4
  import { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './validate.js';
5
- import { trimBlank, sigOfLine, isPlaceholder } from './common.js';
5
+ import { trimBlank, sigOfLine, isPlaceholder, migrateLegacyChild } from './common.js';
6
6
 
7
7
  export function todoTemplate() {
8
8
  // 由 rebuildStructure 生成,保证“模板”与“重排结果”逐字节一致
@@ -156,8 +156,24 @@ function stripStrayBlanks(body, fixed) {
156
156
  }
157
157
 
158
158
  export function enforceBrainFormat(text, spec, prev) {
159
- const src = String(text ?? '');
159
+ let src = String(text ?? '');
160
160
  if (!src.trim()) return { text: src, fixed: [] };
161
+ const fixed = [];
162
+ // ★ 旧 `↳` 子项写法最先迁移(2026-10-20),必须跑在**一切校验之前**:
163
+ // 否则旧行会被 assertTodoContent 判成「裸文本」而拒写 —— 整个库变只读。
164
+ // (实测:真库形态的 ` ↳ 断点: …` 在 load 时就撞上了这条。)
165
+ if (spec?.h1 === '# 📋 Todo Board') {
166
+ const ls0 = src.split('\n');
167
+ let n0 = 0;
168
+ for (let i = 0; i < ls0.length; i++) {
169
+ const conv = migrateLegacyChild(ls0[i]);
170
+ if (conv !== null && conv !== ls0[i]) { ls0[i] = conv; n0++; }
171
+ }
172
+ if (n0) {
173
+ src = ls0.join('\n');
174
+ fixed.push(`旧 ↳ 子项写法 → 标准 md 缩进子项(${n0} 行)`);
175
+ }
176
+ }
161
177
  // 内容闸门:形状归一之前先拒掉乱塞(超长断点/报告体),否则下面的 rebuild
162
178
  // 会把它当「人自加的正文」好好保留下来 —— 越规整越难发现。
163
179
  // ★ 存量宽容(2026-10-05 本机实测踩到):只拒「本次写入新引入」的违规;
@@ -203,7 +219,6 @@ export function enforceBrainFormat(text, spec, prev) {
203
219
  );
204
220
  }
205
221
  };
206
- const fixed = [];
207
222
  // 旧标记 → 标准标记(H1 与分区/分组标题)。与 store.js 的 LEGACY_MARKS 同源,
208
223
  // 由调用方通过 spec.renames 注入(todo.js 不反向依赖 store.js)。
209
224
 
@@ -233,13 +248,23 @@ export function enforceBrainFormat(text, spec, prev) {
233
248
  // 底层 addTask/upsertTask 传的就是裸文本,它们是内部 API,不是 AI 入口)。
234
249
  if (fname === 'todo.md') {
235
250
  const ls = body.split('\n');
251
+ let migrated = 0;
236
252
  for (let i = 0; i < ls.length; i++) {
253
+ // ★ 旧 `↳ 断点:` / `↳ [ ]` 写法 → 标准 md 缩进子项(2026-10-20)。
254
+ // 为何在闸门里做而不只靠 load:所有写入都过这里,迁移一次就永久生效。
255
+ // 保留这个转换器是「存量不能锁死」的代价 —— 它是全仓唯一还认得 ↳ 的地方。
256
+ const conv = migrateLegacyChild(ls[i]);
257
+ if (conv !== null) {
258
+ if (conv !== ls[i]) { ls[i] = conv; migrated++; }
259
+ continue;
260
+ }
237
261
  // 裸任务行(`- [ ] id …` 无状态标记)→ 补默认「进行中」
238
262
  if (/^- \[ \] /.test(ls[i]) && !/^- \[ \] \[[^\]]+\]/.test(ls[i])) {
239
263
  const fixed2 = ensureStateMark(ls[i], '进行中');
240
264
  if (fixed2 !== ls[i]) { ls[i] = fixed2; fixed.push('补回缺失的状态标记'); }
241
265
  }
242
266
  }
267
+ if (migrated) fixed.push(`旧 ↳ 子项写法 → 标准 md 缩进子项(${migrated} 行)`);
243
268
  body = ls.join('\n');
244
269
  }
245
270
 
@@ -277,13 +302,18 @@ function normalizeTodoIf(body, spec) {
277
302
  }
278
303
 
279
304
  export function normalizeTodo(text) {
280
- const lines = text.split('\n');
305
+ // ★ 旧 `↳` 子项写法先就地迁移(2026-10-20)。
306
+ // 为何放在 normalizeTodo 而不只放闸门:读路径(readTodo)不过写入闸门,
307
+ // 不在这里迁,旧行就会以「看不见」的状态留在盘上(面板/看板都不认它)。
308
+ // 迁移后走到下面的「已是标准两区」早退分支也没关系 —— 内容已经是新的了。
309
+ const migrated = String(text).split('\n').map((l) => migrateLegacyChild(l) ?? l).join('\n');
310
+ const lines = migrated.split('\n');
281
311
  const has = (name) => lines.some((l) => l.trim() === `## ${name}`);
282
312
  // 已是标准两区制 = 有 Todo 且**没有**任何进行时老分区。
283
313
  // 坑(2026-09-13):曾只判 has('Todo') 就 return —— 而最老格式里也可能有 `## Todo`
284
314
  // (与 `## In Progress` 混用),于是旧文件永不迁移。
285
315
  const legacy = ['Backlog', 'Today / In Progress', 'In Progress', 'Blocked'].some(has);
286
- if (has('Todo') && !legacy) return text;
316
+ if (has('Todo') && !legacy) return migrated;
287
317
  const out = ['# 📋 Todo Board'];
288
318
  const grab = (name) => {
289
319
  const items = [];
@@ -308,33 +338,31 @@ export function normalizeTodo(text) {
308
338
  }
309
339
 
310
340
  /** 给任务行补行首状态标记(已有则不覆盖)。`- [ ] id …` → `- [ ] [状态] id …`
311
- * 两种前缀(`- [ ]` / 断点任务 `↳ [ ]`)与缩进都认 —— 否则存量迁移时断点任务补不上标记。 */
341
+ * 缩进与 `- [ ]` 前缀都认(子项也要能补标记)。 */
312
342
  export function ensureStateMark(line, state) {
313
- const m = String(line).match(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)(\[[^\]]+\]\s+)?(.*)$/);
343
+ const m = String(line).match(/^(\s*-\s*\[[ x]\]\s*)(\[[^\]]+\]\s+)?(.*)$/);
314
344
  if (!m) return line;
315
345
  if (m[2] && TASK_STATES.includes(m[2].trim().replace(/[\x5B\x5D]/g, ''))) return line;
316
346
  return `${m[1]}[${state}] ${m[3]}`;
317
347
  }
318
348
 
319
- /** 读任务行的状态标记;两种前缀(`- [ ]` / 断点任务 `↳ [ ]`)与缩进都认。 */
349
+ /** 读任务行的状态标记;缩进与 `- [ ]` 前缀都认。 */
320
350
  export function stateOfTaskLine(line) {
321
- const m = String(line).match(/^\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*\[([^\]]+)\]/);
351
+ const m = String(line).match(/^\s*-\s*\[[ x]\]\s*\[([^\]]+)\]/);
322
352
  return m && TASK_STATES.includes(m[1]) ? m[1] : null;
323
353
  }
324
354
 
325
355
  /** 剥掉任务行的状态标记(`- [ ] [进行中] id …` → `- [ ] id …`),保留前缀与缩进。 */
326
356
 
327
357
  export function stripStateMark(line) {
328
- // 两种前缀都剥:`- [ ] `(普通任务)与 `↳ [ ] `(断点任务,带缩进)。
329
- // 原正则写死 `^- \[[ x]\] ` —— 断点任务(2026-10-16 新增形态)匹配不上,
330
- // 后果是状态标记没被剥掉、markDone 的 `- [ ]`→`- [x]` 也落空(实测:完成行仍是 `[ ]`)。
331
- return String(line).replace(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)\[[^\]]+\]\s*/, '$1');
358
+ // 前缀只剥 `- [ ] `(缩进与状态标记的位置保留)。
359
+ // 旧正则里带 `↳ [ ]` 分支已在 2026-10-20 随旧写法一并移除。
360
+ return String(line).replace(/^(\s*-\s*\[[ x]\]\s*)\[[^\]]+\]\s*/, '$1');
332
361
  }
333
362
 
334
- /** 把未完成任务行的复选框从 `[ ]` 翻到 `[x]`(保留原前缀与缩进)。
335
- * 单独抽出:断点任务的 `↳ ` 前缀 + 缩进必须原样保留,用固定的 `- [ ]` 字面替换会漏。 */
363
+ /** 把未完成任务行的复选框从 `[ ]` 翻到 `[x]`(保留原前缀与缩进)。 */
336
364
  export function checkTaskBox(line) {
337
- return String(line).replace(/^(\s*(?:-\s*|↳\s*))\[ \]/, '$1[x]');
365
+ return String(line).replace(/^(\s*-\s*)\[ \]/, '$1[x]');
338
366
  }
339
367
 
340
368
  /** 从已完成任务行提取 `(完成 YYYY-MM-DD)` 日期;无则返回 ''。兼容中英文括号。 */
package/src/todo/tasks.js CHANGED
@@ -45,9 +45,8 @@ export function setStateMark(text, id, state) {
45
45
  const cur = lines[idx];
46
46
  if (stateOfTaskLine(cur) === state) return { text, changed: [] };
47
47
  const raw = stripStateMark(cur); // 先剥旧标记,再补新的
48
- // 前缀两种形态都要能接回(`- [ ]` 与断点任务 `↳ [ ]`),并保留原缩进 ——
49
- // 原正则写死 `^(- \[[ x]\] )`,断点任务上补不回标记(2026-10-16 实测:state 报成功但盘上没变)。
50
- lines[idx] = raw.replace(/^(\s*(?:-\s*\[[ x]\]|↳\s*\[[ x]\])\s*)/, `$1[${state}] `);
48
+ // 前缀统一为 `- [ ]`(缩进保留);旧 `↳` 写法已由 migrateLegacyChild 迁移掉。
49
+ lines[idx] = raw.replace(/^(\s*-\s*\[[ x]\]\s*)/, `$1[${state}] `);
51
50
  return { text: lines.join('\n'), changed: [`${id} 状态 → ${state}`] };
52
51
  }
53
52
 
@@ -121,19 +120,18 @@ export async function upsertTask(brainRoot, { section, text, session = '' }) {
121
120
  : { file: p, text, updated: false };
122
121
  }
123
122
 
124
- /** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。
125
- * 两种前缀都认:`- [ ] `(普通任务)与 `↳ [ ] `(断点任务)。缩进不影响取 id。 */
123
+ /** 从任务文本提取幂等键(行首 id,如 TASK-1 / T-2 / fix-hook)。 */
126
124
 
127
125
  export function idOfTaskLine(line) {
128
126
  if (!line) return null;
129
127
  // 两区制下行首可带状态标记(`- [ ] [进行中] <id> …`)—— 必须先剥掉再取 id,
130
128
  // 否则 id 会被读成 "[进行中]",findTaskLine 全等比对永远落空(2026-09-13 实测)。
131
- // 前缀允许缩进(` - [ ]` / ` ↳ [ ]`)—— 断点任务天然带缩进。
132
- const m = line.match(/^\s*(?:-\s*\[ \]|↳\s*\[ \])\s*(?:\[[^\]]+\]\s*)?(\S+)/);
129
+ // 前缀允许缩进(` - [ ]`)—— 子任务天然带缩进。
130
+ const m = line.match(/^\s*-\s*\[ \]\s*(?:\[[^\]]+\]\s*)?(\S+)/);
133
131
  return m ? m[1].replace(/\u200b/g, '') : null;
134
132
  }
135
133
 
136
- /** 定位 id 对应的未完成任务行下标;真正的附属断点行(`↳ 断点:`)不算任务行。
134
+ /** 定位 id 对应的未完成任务行下标;子项行不算独立任务。
137
135
  * 全等比对,不用 includes(否则 T1 会误命中 T11)。 */
138
136
 
139
137
  export function findTaskLine(lines, id) {
@@ -160,8 +158,7 @@ function childRange(lines, idx) {
160
158
  *
161
159
  * 语义(2026-10-16 用户定):子项就是 todo 的嵌套子任务 —— 可多条、可各自勾选。
162
160
  * `abs todo note <id> "…"` **追加**一条(不再覆盖)—— 用户原话「不限,写多少算多少」。
163
- * 仍保留幂等:与最后一条完全同文就不重复追加(防手滑重复调)。
164
- * 旧的 `↳ 断点:` 存量行不动(parseChild 认得,继续显示);新写一律用标准 md。 */
161
+ * 仍保留幂等:与最后一条完全同文就不重复追加(防手滑重复调)。 */
165
162
  export async function setBreakpoint(brainRoot, { id, text }) {
166
163
  const p = await ensureTodo(brainRoot);
167
164
  let added = false;
@@ -187,8 +184,7 @@ export async function setBreakpoint(brainRoot, { id, text }) {
187
184
  /** 勾/取消勾子项:`- [ ] …` ↔ `- [x] …`。
188
185
  *
189
186
  * 定位:子项没有自己的 id(身份 = 「某任务的第 N 条子项」),故按 n 定位(1-based)。
190
- * n 缺省 = 勾最后一条(最常用的「刚写的这条做完了」)。
191
- * 旧格式 `↳ 断点:` 也能勾 —— 勾一下就地升级成标准 md 形态。 */
187
+ * n 缺省 = 勾最后一条(最常用的「刚写的这条做完了」)。 */
192
188
  export async function setBreakpointDone(brainRoot, { id, n, done = true }) {
193
189
  const p = await ensureTodo(brainRoot);
194
190
  let picked = null;
@@ -91,12 +91,11 @@ export function checkFileShape(file, text, { lintMode = false } = {}) {
91
91
  let p = i - 1;
92
92
  while (p >= 0 && !lines[p].trim()) p--;
93
93
  const prev = p >= 0 ? lines[p] : null;
94
- // 父行可以是:顶层任务 `- [x]`、子任务 ` - [x]`、或旧格式一等任务 ` ↳ [ ] …`
95
- // (后者缩进是历史形态,它本身是任务不是子项 —— 实测漏认会把合法数据误判成孤儿)。
94
+ // 父行可以是:顶层任务 `- [x]` 或子任务 ` - [x]`。
95
+ // (旧 `↳ [ ]` 形态已由 migrateLegacyChild 迁移,不再存在)
96
96
  const ok = prev !== null && (
97
97
  /^- \[[ x]\]/.test(prev) ||
98
- /^\s+- \[[ x]\]/.test(prev) ||
99
- /^\s*↳\s*\[[ x]\]/.test(prev)
98
+ /^\s+- \[[ x]\]/.test(prev)
100
99
  );
101
100
  if (!ok) {
102
101
  issues.push({
@@ -233,28 +232,28 @@ export function assertTodoContent(text, prevSig = null) {
233
232
  const body = bp.text;
234
233
  if (body.length > BREAKPOINT_MAX) {
235
234
  throw new Error(
236
- `✗ 断点太长(第 ${lineNo} 行 ${body.length} 字 > ${BREAKPOINT_MAX})—— 断点只写「改到哪个文件哪一步」。\n` +
235
+ `✗ 子项太长(第 ${lineNo} 行 ${body.length} 字 > ${BREAKPOINT_MAX})—— 子项只写「改到哪个文件哪一步」。\n` +
237
236
  ` 写不下的内容请分流:实施报告/验证数据 → abs log;经验与坑 → abs note;长设计 → .brain/sources/ 页。\n` +
238
237
  ` 原文开头: ${body.slice(0, 60)}...`,
239
238
  );
240
239
  }
241
240
  if (REPORT_MARKERS.some((re) => re.test(body)) || sentenceCount(body) > 2) {
242
241
  throw new Error(
243
- `✗ 断点里像是塞了报告/清单(第 ${lineNo} 行)—— 断点是一句话(最多两句),不是文档。\n` +
242
+ `✗ 子项里像是塞了报告/清单(第 ${lineNo} 行)—— 子项是一句话(最多两句),不是文档。\n` +
244
243
  ` todo 只能加 todo:要做的拆成任务,做完的写 abs log,经验写 abs note。\n` +
245
244
  ` 原文开头: ${body.slice(0, 60)}...`,
246
245
  );
247
246
  }
248
247
  continue;
249
248
  }
250
- // 裸文本行(2026-10-11):Todo 区里既不是任务行也不是断点的行 = 总结/报告/清单。
249
+ // 裸文本行(2026-10-11):Todo 区里既不是任务行也不是子项的行 = 总结/报告/清单。
251
250
  // 前面的修法都挂在「行的形态」上(超长断点、多句断点),把总结写成裸文本行就全部绕过
252
251
  // ——同一个坑第三次复发。改成白名单后,换形态也落回这里。
253
252
  // 按区判:Todo 区才管;Done/Archived 区有日期分组与归档链接,判据不同。
254
253
  // 存量行(prevSig)放行,交给 lint 报 —— 否则老库直接被锁死写不进去。
255
254
  if (todoSectionOf(lines, i) === 'Todo' && !isAllowedTodoLine(l) && !isLegacy(l)) {
256
255
  throw new Error(
257
- `✗ todo 的 Todo 区不允许裸文本(第 ${lineNo} 行)—— 这里只放任务行(- [ ] …)与断点行(↳ 断点: …)。\n` +
256
+ `✗ todo 的 Todo 区不允许裸文本(第 ${lineNo} 行)—— 这里只放任务行(- [ ] …)与子项( - [ ] …)。\n` +
258
257
  ` 「总结/实施报告/清单」请分流:流水与测试数据 → abs log;经验与坑 → abs note;长设计与方案 → .brain/sources/ 页。\n` +
259
258
  ` 原文开头: ${l.trim().slice(0, 60)}...`,
260
259
  );
package/src/todo.js CHANGED
@@ -8,3 +8,4 @@ export { todoTemplate, rebuildStructure, dropRetiredSections, enforceBrainFormat
8
8
  export { checkFileShape, violationSignatures, assertNoStrayBlank, assertTodoContent } from './todo/validate.js';
9
9
  export { doneDateOf, doneKindOf, withDoneKind, collapseDone, archiveDoneInText, renderArchiveBody, upsertArchiveSection, groupDoneSection, insertDoneGrouped } from './todo/archive.js';
10
10
  export { readTodo, ensureTodo, setStateMark, addTask, upsertTask, idOfTaskLine, findTaskLine, setBreakpoint, setBreakpointDone, setMemo, readMemo, boardText } from './todo/tasks.js';
11
+ export { migrateLegacyChild, isTaskLine, isChildLine, CHILD_INDENT } from './todo/common.js';
package/src/wrapup.js CHANGED
@@ -37,7 +37,7 @@ async function rotateIfNeeded(p) {
37
37
  }
38
38
 
39
39
  /** 从 todo 文本/快照块提取任务清单。兼容顶层(todo.md `- [ ]`)与缩进(快照 ` - [ ]`)两种行。
40
- * body = 去掉 `- [ ]` 前缀、`(认领|完成 date)` 标注后的核心文本。bp 去掉 `↳ 断点|卡点: ` 前缀。
40
+ * body = 去掉 `- [ ]` 前缀、`(认领|完成 date)` 标注后的核心文本。bp 去掉 `[ ]`/`[x]` 勾选前缀。
41
41
  * Done 区(## Done 下)不采。返回 [{ body, bp }],bp 为附属断点数组(或空)。 */
42
42
  export function extractOpenTasks(todoText) {
43
43
  const lines = String(todoText || '').split('\n');
@@ -60,16 +60,18 @@ export function extractOpenTasks(todoText) {
60
60
  .trim();
61
61
  if (taskBody && !isPlaceholderBody(taskBody)) {
62
62
  const bp = [];
63
- while (i + 1 < lines.length && /^\s*↳/.test(lines[i + 1])) {
63
+ // 子项:缩进 `- [ ]`(标准 md)。旧快照里的 `↳` 行也能读 —— 那是历史
64
+ // wrapup.log 的序列化格式,不能因为改了新格式就让旧快照的日志读不出来。
65
+ while (i + 1 < lines.length && (/^\s+-\s*\[[ x]\]/.test(lines[i + 1]) || /^\s*↳/.test(lines[i + 1]))) {
64
66
  i++;
65
- // 断点快照带上勾选标记(`[ ] 内容` / `[x] 内容`)—— 快照是给人看的,
66
- // 不保留勾选就分不清「当时已完成」与「还没做」(2026-10-16 断点可勾后必需)。
67
+ // 子项快照带上勾选标记(`[ ] 内容` / `[x] 内容`)—— 快照是给人看的,
68
+ // 不保留勾选就分不清「当时已完成」与「还没做」。
67
69
  const parsed = parseChild(lines[i]);
68
70
  if (parsed) {
69
71
  bp.push(`${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`);
70
72
  } else {
71
- // 非断点的 ↳ 行(如 `↳ [ ] 子任务`)仍照旧剥前缀
72
- bp.push(lines[i].trim().replace(/^↳\s*(断点|卡点)?[::]?\s*/, ''));
73
+ // 旧快照的非标准 ↳ 行:剥掉 ↳ 与可选标签,内容原样保留
74
+ bp.push(lines[i].trim().replace(/^↳\s*(断点|卡点|内容)?[::]?\s*/, ''));
73
75
  }
74
76
  }
75
77
  out.push({ body: taskBody, bp });
@@ -85,13 +87,16 @@ function isPlaceholderBody(body) {
85
87
  return /^(待办任务|无|(无)|None|null)$/.test(body) || /^待办任务/.test(body);
86
88
  }
87
89
 
88
- /** 序列化一个快照块:首行时间戳+归属,后随未完成任务(主体+断点)。
89
- * 断点 bp 元素已自带勾选前缀(`[ ] 内容` / `[x] 内容`,见 extractOpenTasks)。 */
90
+ /** 序列化一个快照块:首行时间戳+归属,后随未完成任务(主体+子项)。
91
+ * 子项 bp 元素已自带勾选前缀(`[ ] 内容` / `[x] 内容`,见 extractOpenTasks)。
92
+ * 新写入一律用标准 md 缩进子项(旧 `↳ 断点:` 形态已废弃)。 */
90
93
  function blockText(root, tasks) {
91
94
  const head = `[${localStamp()}] wrapup proj=${root}`;
92
95
  if (!tasks.length) return head; // 无滞留,仍记一行「干净结束」
93
96
  const rows = tasks.map((t) => {
94
- const bps = t.bp.map((b) => (b.startsWith('[') ? ` ↳ ${b}` : ` ↳ 断点: ${b}`));
97
+ const bps = t.bp.map((b) => (b.startsWith('[')
98
+ ? ` - ${b}`
99
+ : ` - [ ] ${b}`));
95
100
  return ` - [ ] ${t.body}${bps.length ? '\n' + bps.join('\n') : ''}`;
96
101
  });
97
102
  return [head, ...rows].join('\n');
@@ -119,13 +124,14 @@ export function parseWrapup(text) {
119
124
  }
120
125
  continue;
121
126
  }
122
- const bm = line.match(/^\s*↳/);
127
+ // 子项:新快照写缩进 `- [ ]`;旧快照(历史 wrapup.log)写的是 `↳ …`,两者都要读
128
+ const bm = line.match(/^\s+-\s*\[[ x]\]/) || line.match(/^\s*↳/);
123
129
  if (bm && cur.tasks.length) {
124
130
  // 回读时统一成 `[ ] 内容` / `[x] 内容` 形态(新写的就是这个;旧快照无勾 → 当未勾)
125
131
  const parsed = parseChild(line);
126
132
  cur.tasks[cur.tasks.length - 1].bp.push(parsed
127
133
  ? `${parsed.done ? '[x]' : '[ ]'} ${parsed.text}`
128
- : line.trim().replace(/^↳\s*(断点|卡点)?[::]?\s*/, ''));
134
+ : line.trim().replace(/^↳\s*(断点|卡点|内容)?[::]?\s*/, ''));
129
135
  }
130
136
  }
131
137
  return byProj;