Skip to content

Commit 6d4787d

Browse files
committed
chore(scripts): 行数生成器理由续行与解析口径合并
autoReason 由「只取 @ai-context 首个物理行」改为续读到句末/空行/注释块收尾 (// //! /// * /* 前缀逐行剥离,句末只认 。!?),并新增 humanWhy:带 「(自动摘取,待细化)」标记的单元格是生成物 ⇒ 每次 --write 重摘,否则旧 截断文本会被当成人工理由永久固化。parseTable 与 parseReasons 合并为单一 解析器(查表/写表同一处口径)。脚本 284 → 300 行(≤300);--full 仍不写表。 登记表刷新与「已拆分」节复核标注已随并行提交 d85d9fe / e962b22 落库。
1 parent e962b22 commit 6d4787d

1 file changed

Lines changed: 51 additions & 35 deletions

File tree

‎scripts/line-limits.mjs‎

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* 读者无法判断哪行有效)、且 15 个 >600 违规中有 10 个被登记表用 <600 的数字
77
* 「认证为合规」。故把「测量」收进一个脚本:写表与查表共用同一套逻辑,口径只有一处实现。
88
*
9-
* 副作用:`--write` 会重写 docs/standards/line-limit-exemptions.md(人工维护的
10-
* 「豁免理由 / 拆分计划」两列按路径保留);默认模式只读。
9+
* 副作用:`--write` 会重写 docs/standards/line-limit-exemptions.md(人工列按路径保留,唯带
10+
* `(自动摘取,待细化)` 标记的理由是生成物、每次重摘);默认模式只读。
1111
* 边界:只扫 app/src 与 app/src-tauri/src 下的 .ts/.tsx/.rs。>600 用**棘轮**而非
1212
* 「立刻为零」—— 拆分是 0-C2/C3 的事,本脚本负责让违规**不再增加**且**可见**。
1313
*
@@ -68,43 +68,53 @@ export function scanTree() {
6868
return out;
6969
}
7070

71-
/** 从登记表抽出条目:| 路径 | 行数 | … | */
71+
/**
72+
* 从登记表抽出条目行:| 路径 | 行数 | 豁免理由 | 拆分计划 | —— **查表与写表共用这一处解析口径**。
73+
* Why 合并(原为 parseTable / parseReasons 两份近似正则):两者解析的是同一张表,改一处忘一处就会让
74+
* `check` 与 `--write` 对行数/理由的理解漂移;且只有 4 列全取,写入口才能保留人工维护的两列。
75+
*/
7276
export function parseTable() {
7377
const abs = join(ROOT, TABLE_PATH);
7478
if (!existsSync(abs)) return [];
7579
const rows = [];
7680
for (const line of readFileSync(abs, 'utf8').split('\n')) {
77-
const m = /^\|\s*`?([^|`]+?\.(?:rs|tsx?))`?\s*\|\s*(\d+)\s*\|/.exec(line);
78-
if (m) rows.push({ path: m[1].trim(), declared: Number(m[2]) });
81+
const m = /^\|\s*`?([^|`]+?\.(?:rs|tsx?))`?\s*\|\s*(\d+)\s*\|\s*([^|]*)\|\s*([^|]*)\|/.exec(line);
82+
if (m) rows.push({ path: m[1].trim(), declared: Number(m[2]), why: m[3].trim(), split: m[4].trim() });
7983
}
8084
return rows;
8185
}
8286

83-
/** 从现有登记表按路径抽出人工维护的两列(生成器保留它们) */
84-
function parseReasons() {
85-
const abs = join(ROOT, TABLE_PATH);
86-
const map = new Map();
87-
if (!existsSync(abs)) return map;
88-
for (const line of readFileSync(abs, 'utf8').split('\n')) {
89-
const m = /^\|\s*`?([^|`]+?\.(?:rs|tsx?))`?\s*\|\s*\d+\s*\|\s*([^|]*)\|\s*([^|]*)\|/.exec(line);
90-
if (m) map.set(m[1].trim(), { why: m[2].trim(), split: m[3].trim() });
91-
}
92-
return map;
93-
}
87+
/** 自动理由的**来源标记**:带它的单元格是生成物 ⇒ 每次 `--write` 重新摘取(见 `humanWhy`) */
88+
const AUTO_SUFFIX = '(自动摘取,待细化)';
89+
/** 注释行前缀(`//` `//!` `///` `*` `/*`)—— 续读时逐行剥离;无前缀的行(裸代码)即注释块已结束 */
90+
const COMMENT_EDGE = /^[ \t]*(?:\/\/[/!]*|\/\*+|\*+\/?)[ \t]?/;
91+
/** 续读上限:当前最长自然终止(`。!?` 句末)需 11 行,12 只作防御(防无空行无句末的注释块被整块吞进单元格) */
92+
const REASON_MAX_LINES = 12;
9493

95-
/** 取该文件 `@ai-context` 的首行要点,作为自动补登时的理由 */
94+
/**
95+
* 取该文件 `@ai-context` 的**首句**要点,作为自动补登时的理由。
96+
* Why 抓整行再清洗:本仓大量 `@ai-context` 行以 `**加粗**` 开头(如 `@ai-context **域图标**几何(9 个):…`),
97+
* 用 `[^\n*]+` 直接卡在 `*` 上会**一格都捕获不到**,使自动理由全部退化成占位符。
98+
* Why 续读:`@ai-context` 常折行(首行停在 `(`/`——`/`+` 等连接符处)⇒ 只取第一个物理行会让理由半句截断
99+
* (2026-09-11 实测 43 条里 41 条如此)。续读到**句末 / 空行 / 注释块收尾**;句末只认 `。!?` —— 把 `;` 也算句末会停在未闭合括号里(实测 122 条有 1 条)。
100+
*/
96101
function autoReason(absPath) {
97-
const src = readFileSync(absPath, 'utf8');
98-
// 捕获**整行**再清洗,而不是用 `[^\n*]+` 直接卡在 `*` 上 —— 本仓大量 `@ai-context` 行以
99-
// `**加粗**` 开头(如 `@ai-context **域图标**几何(9 个):…`),卡 `*` 会**一格都捕获不到**,
100-
// 使 45 条自动理由全部退化成占位符。
101-
const m = /@ai-context[::]?\s*([^\n]+)/.exec(src);
102-
if (!m) return '(待补理由:本条目由生成器补登)';
103-
const text = m[1]
104-
.replace(/\*\/\s*$/, '') // 单行块注释的收尾 `*/`
105-
.replace(/\*\*/g, '') // 加粗标记
106-
.trim();
107-
return text ? `${text}(自动摘取,待细化)` : '(待补理由:本条目由生成器补登)';
102+
const lines = readFileSync(absPath, 'utf8').split('\n');
103+
const at = lines.findIndex((l) => l.includes('@ai-context'));
104+
if (at < 0) return '(待补理由:本条目由生成器补登)';
105+
const parts = [];
106+
for (let i = at; i < lines.length && i < at + REASON_MAX_LINES; i++) {
107+
const raw = lines[i];
108+
const close = raw.indexOf('*/'); // 行内块注释收尾:只取它前面的文字
109+
const bare = close < 0 ? raw : raw.slice(0, close);
110+
if (i > at && !COMMENT_EDGE.test(bare)) break; // 无注释前缀 = 注释块已结束
111+
// 首行只取 `@ai-context` 之后的文字;续行剥注释前缀(折行处不补空格 —— 本仓注释在中英混排处断行)
112+
const body = (i === at ? bare.replace(/^[\s\S]*?@ai-context[::]?\s*/, '') : bare.replace(COMMENT_EDGE, '')).trim();
113+
if (body) parts.push(body);
114+
if (!body || close >= 0 || /[。!?]$/.test(body)) break; // 空行 / 块注释收尾 / 句末
115+
}
116+
const text = parts.join('').replace(/\*\*/g, '').trim();
117+
return text ? `${text}${AUTO_SUFFIX}` : '(待补理由:本条目由生成器补登)';
108118
}
109119

110120
/**
@@ -133,7 +143,7 @@ function assertScanSane(measured) {
133143
}
134144
}
135145
// 覆盖断言(上面两条的补集):SCAN_DIRS **少写一个目录**时列出的目录都存在且有命中 ⇒ 上面全过,但棘轮/登记表里的文件整体落到扫描域外 ⇒ (b)/(d) 报成"→ 删除该行"、--write 整片删掉仍打印 ✅(实测 ['app/src']:10×(b)+102×(d);--write exit 0,179 → 77 行 / 138 → 36 条)。
136-
const declared = new Set([...FROZEN_OVER_LIMIT, ...parseReasons().keys()]);
146+
const declared = new Set([...FROZEN_OVER_LIMIT, ...parseTable().map((r) => r.path)]);
137147
const uncovered = [...declared].filter((p) => !SCAN_DIRS.some((d) => p.startsWith(`${d}/`)));
138148
if (uncovered.length) {
139149
console.error(
@@ -155,15 +165,21 @@ function parseHistory() {
155165

156166
function writeTable() {
157167
const measured = scanTree();
158-
// ⚠️ 必须在 parseReasons()/writeFileSync() **之前**:否则扫描域失效时它会把 138 条清成表头 + 历史节(实测 179 → 41 行)还打印 ✅、exit 0。同类破坏在"SCAN_DIRS 少写一个目录"时同样可达。
168+
// ⚠️ 必须在 parseTable()/writeFileSync() **之前**:否则扫描域失效时它会把 138 条清成表头 + 历史节(实测 179 → 41 行)还打印 ✅、exit 0。同类破坏在"SCAN_DIRS 少写一个目录"时同样可达。
159169
assertScanSane(measured);
160-
const reasons = parseReasons();
170+
const reasons = new Map(parseTable().map((r) => [r.path, r]));
171+
// 带 AUTO_SUFFIX 的单元格是**生成物** ⇒ 重新摘取。少了这一步,旧实现留下的半句截断文本会被当成
172+
// 人工理由在 `parseTable` 里永久固化,`autoReason` 的改进就永远落不到表上(实测 43 条全带该标记)。
173+
const humanWhy = (p) => {
174+
const w = reasons.get(p)?.why ?? '';
175+
return w.endsWith(AUTO_SUFFIX) ? '' : w;
176+
};
161177
// 并列时必须按路径断开:`scanTree` 的 Map 迭代序来自 readdirSync,**跨平台不一致**
162178
// (Windows 与 Linux 的顺序可能不同)⇒ 只按行数排会让 `--write` 在不同平台产出不同字节。
163179
const byLinesDesc = (a, b) => b[1] - a[1] || a[0].localeCompare(b[0]);
164180
const over = [...measured.entries()].filter(([, n]) => n > HARD_LIMIT).sort(byLinesDesc);
165181
const band = [...measured.entries()].filter(([, n]) => n > SOFT_LIMIT && n <= HARD_LIMIT).sort(byLinesDesc);
166-
// 单元格净化:文本里的半角 `|` 会撑破 Markdown 表格,并让下次 `parseReasons` 误切列。
182+
// 单元格净化:文本里的半角 `|` 会撑破 Markdown 表格,并让下次 `parseTable` 误切列。
167183
// 换**全角** `|` 而不是 `\|` —— 转义写法在下次读取时会被再次转义,破坏 `--write` 的幂等性。
168184
const cell = (t, fallback) => {
169185
const s = (t ?? '').replace(/\|/g, '|').replace(/\s*\n\s*/g, ' ').trim();
@@ -183,7 +199,7 @@ function writeTable() {
183199
'',
184200
'> ⚠️ **本文件是生成物** —— 行数与条目成员关系由 `node scripts/line-limits.mjs --write` 生成,',
185201
'> **不要手改数字、手加行或手删行**(会被 `node scripts/line-limits.mjs` 判为违规)。',
186-
'> 「豁免理由」「拆分计划」两列由**人工**维护,生成器按路径保留;「已拆分 / 登记移除记录」节逐字保留。',
202+
'> 「豁免理由」「拆分计划」两列由**人工**维护,生成器按路径保留(唯带 `(自动摘取,待细化)` 标记的是生成物、每次重摘);「已拆分 / 登记移除记录」节逐字保留。',
187203
'>',
188204
'> **测量口径(唯一有效)**:文件**全部行数**(含空行),等价于 `[System.IO.File]::ReadAllLines(path, UTF8).Count`。',
189205
'> ⚠️ **禁用** `Get-Content` 数行(本机 PowerShell 5.1 + 码页 `gb2312` 会按 GBK 解码、吞换行、**少算**)与 `Measure-Object -Line`(**只数非空行**)。',
@@ -194,7 +210,7 @@ function writeTable() {
194210
'',
195211
'> 本表受棘轮守卫保护:**只允许减少**。每完成一个拆分,从 `scripts/line-limits.mjs` 的 `FROZEN_OVER_LIMIT` 删掉对应一行。',
196212
'',
197-
// ⚠️ 本表**保持 4 列**(与下一节同形),不要"顺手"简化成 3 列 —— `parseReasons` 用 4 列正则
213+
// ⚠️ 本表**保持 4 列**(与下一节同形),不要"顺手"简化成 3 列 —— `parseTable` 用 4 列正则
198214
// 按路径保留人工维护的两列,改成 3 列会让这些拆分计划在**下一次重生成时静默丢失**。
199215
'| 文件 | 行数 | 说明 | 拆分计划 |',
200216
'|---|---|---|---|',
@@ -206,7 +222,7 @@ function writeTable() {
206222
'|---|---|---|---|',
207223
...band.map(([p, n]) => {
208224
const r = reasons.get(p);
209-
return row(p, n, r?.why || autoReason(join(ROOT, p)), r?.split);
225+
return row(p, n, humanWhy(p) || autoReason(join(ROOT, p)), r?.split);
210226
}),
211227
'',
212228
parseHistory(),

0 commit comments

Comments
 (0)