@dommaker/harness 0.16.8 → 0.16.9
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/CHANGELOG.md +7 -0
- package/bin/harness.js +1 -0
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +14 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +77 -9
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts +2 -0
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +83 -20
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/core/constraints/capabilities-parser.d.ts +7 -0
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +17 -0
- package/dist/core/constraints/capabilities-parser.js.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.d.ts +3 -0
- package/dist/core/constraints/checkers/capability-sync.d.ts.map +1 -1
- package/dist/core/constraints/checkers/capability-sync.js +30 -1
- package/dist/core/constraints/checkers/capability-sync.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +4 -2
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +11 -1
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +20 -0
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/types/project-config.d.ts +14 -0
- package/dist/types/project-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/checker-extra.test.ts +130 -0
- package/src/__tests__/project-config-loader.test.ts +40 -1
- package/src/cli/commands/__tests__/sync-docs.test.ts +163 -0
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +85 -8
- package/src/cli/commands/sync-docs/index.ts +87 -24
- package/src/core/constraints/capabilities-parser.ts +15 -0
- package/src/core/constraints/checkers/capability-sync.ts +30 -2
- package/src/core/constraints/definitions/guidelines.ts +4 -2
- package/src/core/project-config-loader.ts +24 -0
- package/src/types/project-config.ts +16 -0
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import * as fs from 'fs/promises';
|
|
10
|
+
import * as path from 'path';
|
|
10
11
|
import { IRON_LAWS, GUIDELINES, TIPS } from '../../../core/constraints/definitions';
|
|
11
12
|
import { readCapabilitiesEntries } from '../../../core/constraints/capabilities-parser';
|
|
12
13
|
import { FreshnessRunner } from '../../../core/constraints/doc-freshness/runner';
|
|
14
|
+
import type { CapabilitiesMode } from '../../../core/project-config-loader';
|
|
13
15
|
import type { DocFreshnessCheck } from '../../../types/project-config';
|
|
14
16
|
import type { ModuleInfo, SyncResult } from './project-reader';
|
|
15
17
|
|
|
@@ -137,19 +139,23 @@ export function updateCapabilityCounts(content: string, projectPath: string): st
|
|
|
137
139
|
|
|
138
140
|
/**
|
|
139
141
|
* 更新 CAPABILITIES.md 文件
|
|
142
|
+
*
|
|
143
|
+
* module 模式(governance.capabilities.mode=module)下不再为新文件自动加表格行
|
|
144
|
+
* (目录条目需人工策划登记),幽灵行剔除与「最后更新」刷新保留。
|
|
140
145
|
*/
|
|
141
146
|
export async function updateCapabilitiesFile(
|
|
142
147
|
capabilitiesPath: string,
|
|
143
148
|
currentModules: ModuleInfo[],
|
|
144
149
|
existingFiles: string[],
|
|
145
150
|
result: SyncResult,
|
|
151
|
+
mode: CapabilitiesMode = 'file',
|
|
146
152
|
): Promise<void> {
|
|
147
153
|
let content: string;
|
|
148
154
|
try {
|
|
149
155
|
content = await fs.readFile(capabilitiesPath, 'utf-8');
|
|
150
156
|
} catch {
|
|
151
|
-
//
|
|
152
|
-
content = generateCapabilitiesContent(currentModules);
|
|
157
|
+
// 文件不存在,创建新的(module 模式生成按目录聚合的模板)
|
|
158
|
+
content = generateCapabilitiesContent(currentModules, mode);
|
|
153
159
|
await fs.writeFile(capabilitiesPath, content, 'utf-8');
|
|
154
160
|
return;
|
|
155
161
|
}
|
|
@@ -164,8 +170,8 @@ export async function updateCapabilitiesFile(
|
|
|
164
170
|
content = content.replace(rowRegex, '');
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
//
|
|
168
|
-
if (result.added.length > 0) {
|
|
173
|
+
// 添加新文件的行(在最后一个表格行之后);module 模式跳过
|
|
174
|
+
if (mode !== 'module' && result.added.length > 0) {
|
|
169
175
|
const getBasenameLocal = (f: string) => f.split('/').pop()!;
|
|
170
176
|
const addedModules = currentModules.filter(m => result.added.includes(getBasenameLocal(m.file)));
|
|
171
177
|
const tableEndRegex = /(^\|[^|]+\|[^|]+\|[^|]+\|\s*$)/gm;
|
|
@@ -186,8 +192,8 @@ export async function updateCapabilitiesFile(
|
|
|
186
192
|
// 清理多余空行
|
|
187
193
|
content = content.replace(/\n{3,}/g, '\n\n');
|
|
188
194
|
} else {
|
|
189
|
-
//
|
|
190
|
-
content += '\n\n' + generateModuleTable(currentModules);
|
|
195
|
+
// 没有表格,追加模块表格(module 模式按目录聚合)
|
|
196
|
+
content += '\n\n' + (mode === 'module' ? generateDirTable(currentModules) : generateModuleTable(currentModules));
|
|
191
197
|
}
|
|
192
198
|
|
|
193
199
|
// 更新最后更新时间
|
|
@@ -203,7 +209,7 @@ export async function updateCapabilitiesFile(
|
|
|
203
209
|
/**
|
|
204
210
|
* 生成 CAPABILITIES.md 内容
|
|
205
211
|
*/
|
|
206
|
-
function generateCapabilitiesContent(modules: ModuleInfo[]): string {
|
|
212
|
+
function generateCapabilitiesContent(modules: ModuleInfo[], mode: CapabilitiesMode = 'file'): string {
|
|
207
213
|
const now = new Date().toISOString().split('T')[0];
|
|
208
214
|
return `# CAPABILITIES.md
|
|
209
215
|
|
|
@@ -211,7 +217,7 @@ function generateCapabilitiesContent(modules: ModuleInfo[]): string {
|
|
|
211
217
|
|
|
212
218
|
---
|
|
213
219
|
|
|
214
|
-
${generateModuleTable(modules)}
|
|
220
|
+
${mode === 'module' ? generateDirTable(modules) : generateModuleTable(modules)}
|
|
215
221
|
`;
|
|
216
222
|
}
|
|
217
223
|
|
|
@@ -227,3 +233,74 @@ function generateModuleTable(modules: ModuleInfo[]): string {
|
|
|
227
233
|
|
|
228
234
|
return `| 模块 | 文件 | 说明 |\n|------|------|------|\n${rows}`;
|
|
229
235
|
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 生成按目录聚合的模块表格(module 模式模板)
|
|
239
|
+
*
|
|
240
|
+
* 每个源码子目录一行目录条目(| core | src/core/ | core |),
|
|
241
|
+
* 文件直接位于源码根时聚到根目录一行。
|
|
242
|
+
*/
|
|
243
|
+
function generateDirTable(modules: ModuleInfo[]): string {
|
|
244
|
+
if (modules.length === 0) return '';
|
|
245
|
+
|
|
246
|
+
const dirs: string[] = [];
|
|
247
|
+
for (const m of modules) {
|
|
248
|
+
const dir = path.posix.dirname(m.file);
|
|
249
|
+
const key = dir === '.' ? '' : dir + '/';
|
|
250
|
+
if (key && !dirs.includes(key)) dirs.push(key);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const rows = dirs.map(d => {
|
|
254
|
+
const name = d.replace(/\/$/, '').split('/').pop()!;
|
|
255
|
+
return `| ${name} | ${d} | ${name} |`;
|
|
256
|
+
}).join('\n');
|
|
257
|
+
|
|
258
|
+
return `| 模块 | 文件 | 说明 |\n|------|------|------|\n${rows}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 将 CAPABILITIES.md 文件表格折叠为目录条目(--compact 一次性迁移)
|
|
263
|
+
*
|
|
264
|
+
* 按文件条目(第二列)的 dirname 分组:同组 ≥2 个文件条目折叠为一行目录条目
|
|
265
|
+
* (替换组内第一行、删除其余行,说明取组内第一行,为空则用目录名);
|
|
266
|
+
* 单独成组的文件行、表头、非表格文本、PRESERVE 块原样保留。
|
|
267
|
+
* 已折叠的目录条目行不再匹配文件条目,故幂等。
|
|
268
|
+
*/
|
|
269
|
+
export function compactCapabilitiesContent(content: string): string {
|
|
270
|
+
const lines = content.split('\n');
|
|
271
|
+
const fileEntryRegex = /\.(?:ts|tsx|js|jsx)$/;
|
|
272
|
+
const rowRegex = /^\|[^|]+\|[^|]+\|[^|]+\|\s*$/;
|
|
273
|
+
|
|
274
|
+
interface TableRow {
|
|
275
|
+
lineIndex: number;
|
|
276
|
+
file: string;
|
|
277
|
+
desc: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 收集文件条目行,按 dirname 分组(无目录前缀的条目不参与折叠)
|
|
281
|
+
const groups = new Map<string, TableRow[]>();
|
|
282
|
+
for (let i = 0; i < lines.length; i++) {
|
|
283
|
+
if (!rowRegex.test(lines[i])) continue;
|
|
284
|
+
const cells = lines[i].split('|').map(c => c.trim());
|
|
285
|
+
const file = cells[2] ?? '';
|
|
286
|
+
if (!fileEntryRegex.test(file)) continue;
|
|
287
|
+
const dir = path.posix.dirname(file);
|
|
288
|
+
if (dir === '.') continue;
|
|
289
|
+
const group = groups.get(dir) ?? [];
|
|
290
|
+
group.push({ lineIndex: i, file, desc: cells[3] ?? '' });
|
|
291
|
+
groups.set(dir, group);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const deleteLines = new Set<number>();
|
|
295
|
+
for (const [dir, rows] of groups) {
|
|
296
|
+
if (rows.length < 2) continue;
|
|
297
|
+
const name = dir.split('/').pop()!;
|
|
298
|
+
const desc = rows[0].desc || name;
|
|
299
|
+
lines[rows[0].lineIndex] = `| ${name} | ${dir}/ | ${desc} |`;
|
|
300
|
+
for (const row of rows.slice(1)) {
|
|
301
|
+
deleteLines.add(row.lineIndex);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return lines.filter((_, i) => !deleteLines.has(i)).join('\n');
|
|
306
|
+
}
|
|
@@ -16,8 +16,9 @@ import chalk from 'chalk';
|
|
|
16
16
|
import * as fs from 'fs/promises';
|
|
17
17
|
import { existsSync } from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
|
-
import { readCapabilitiesEntries } from '../../../core/constraints/capabilities-parser';
|
|
19
|
+
import { aggregateToSourceSubdir, readCapabilitiesEntries } from '../../../core/constraints/capabilities-parser';
|
|
20
20
|
import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
21
|
+
import { getCapabilitiesMode } from '../../../core/project-config-loader';
|
|
21
22
|
import { getSourceDirs, scanSourceModules, getRequiredContextDirs } from './project-reader';
|
|
22
23
|
import type { ModuleInfo, SyncResult } from './project-reader';
|
|
23
24
|
import {
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
checkCapabilityCounts,
|
|
27
28
|
updateCapabilityCounts,
|
|
28
29
|
updateCapabilitiesFile,
|
|
30
|
+
compactCapabilitiesContent,
|
|
29
31
|
} from './capabilities-syncer';
|
|
30
32
|
import { createContextMd, findExistingContextFiles, getLatestTsMtime } from './context-syncer';
|
|
31
33
|
import { buildAgentsMd } from './agents-syncer';
|
|
@@ -42,6 +44,8 @@ export interface SyncDocsOptions {
|
|
|
42
44
|
json?: boolean;
|
|
43
45
|
/** 同步 AGENTS.md(agent 导读);PRESERVE 标记段在重新生成时原样保留 */
|
|
44
46
|
agents?: boolean;
|
|
47
|
+
/** 一次性迁移:将 CAPABILITIES.md 文件表格折叠为目录条目 */
|
|
48
|
+
compact?: boolean;
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
@@ -51,6 +55,26 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
51
55
|
const projectPath = options.projectPath || process.cwd();
|
|
52
56
|
const isCheck = options.check === true;
|
|
53
57
|
const isJson = options.json === true;
|
|
58
|
+
const capsMode = getCapabilitiesMode(projectPath);
|
|
59
|
+
|
|
60
|
+
// --compact:一次性迁移,将文件表格折叠为目录条目后直接返回
|
|
61
|
+
if (options.compact) {
|
|
62
|
+
const capsPath = path.join(projectPath, 'CAPABILITIES.md');
|
|
63
|
+
try {
|
|
64
|
+
const content = await fs.readFile(capsPath, 'utf-8');
|
|
65
|
+
const compacted = compactCapabilitiesContent(content);
|
|
66
|
+
if (compacted !== content) {
|
|
67
|
+
await fs.writeFile(capsPath, compacted, 'utf-8');
|
|
68
|
+
if (!isJson) console.log(chalk.green('✅ 已将 CAPABILITIES.md 文件表格折叠为目录条目'));
|
|
69
|
+
} else {
|
|
70
|
+
if (!isJson) console.log(chalk.green('✅ CAPABILITIES.md 无需折叠'));
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
if (!isJson) console.log(chalk.red('❌ CAPABILITIES.md 不存在,无法折叠'));
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
54
78
|
|
|
55
79
|
if (!isJson) {
|
|
56
80
|
if (isCheck) {
|
|
@@ -88,7 +112,8 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
88
112
|
let capsIsCapabilityListing = false;
|
|
89
113
|
try {
|
|
90
114
|
capsContent = await fs.readFile(capabilitiesPath, 'utf-8');
|
|
91
|
-
|
|
115
|
+
// mode=listing 是清单格式的显式声明,与嗅探结果等价
|
|
116
|
+
if (capsMode === 'listing' || isCapabilityListingFormat(capsContent)) {
|
|
92
117
|
capsIsCapabilityListing = true;
|
|
93
118
|
} else {
|
|
94
119
|
existingFiles = await parseCapabilitiesFiles(capabilitiesPath);
|
|
@@ -116,13 +141,31 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
116
141
|
// (basename 列表里永远不会有目录,直接对比会把目录行误报为「已删除的模块」)
|
|
117
142
|
const existingDirs = existingFiles.filter(f => f.endsWith('/'));
|
|
118
143
|
const existingFileNames = existingFiles.filter(f => !f.endsWith('/'));
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
144
|
+
if (capsMode === 'module') {
|
|
145
|
+
// module 模式:文件条目精确匹配或目录条目前缀覆盖;
|
|
146
|
+
// added 不再是逐文件列表,聚合为「未覆盖目录」(与 capability_sync checker 同规则)
|
|
147
|
+
const entries = readCapabilitiesEntries(capabilitiesPath, { includeDirs: true });
|
|
148
|
+
const fileEntries = entries.filter(e => !e.endsWith('/'));
|
|
149
|
+
const dirEntries = entries.filter(e => e.endsWith('/'));
|
|
150
|
+
const uncoveredDirs = new Set<string>();
|
|
151
|
+
for (const m of currentModules) {
|
|
152
|
+
const covered =
|
|
153
|
+
fileEntries.includes(m.file) || dirEntries.some(d => m.file.startsWith(d));
|
|
154
|
+
if (!covered) {
|
|
155
|
+
const root = srcDirs.find(d => m.file.startsWith(d + '/')) || srcDirs[0] || '';
|
|
156
|
+
uncoveredDirs.add(aggregateToSourceSubdir(root, m.file));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
result.added = [...uncoveredDirs];
|
|
160
|
+
} else {
|
|
161
|
+
result.added = currentModules
|
|
162
|
+
.filter(
|
|
163
|
+
m =>
|
|
164
|
+
!existingDirs.some(d => m.file.startsWith(d)) &&
|
|
165
|
+
!existingFileNames.includes(getBasename(m.file))
|
|
166
|
+
)
|
|
167
|
+
.map(m => getBasename(m.file));
|
|
168
|
+
}
|
|
126
169
|
result.removed = existingFileNames.filter(f => !currentBasenames.includes(f));
|
|
127
170
|
|
|
128
171
|
// 幽灵条目清扫(2026-08-08 studio CI 4 连红事故):上方按 basename 对比,
|
|
@@ -235,18 +278,30 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
235
278
|
}
|
|
236
279
|
|
|
237
280
|
if (!capsIsCapabilityListing && hasTableIssues) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
281
|
+
if (capsMode === 'module') {
|
|
282
|
+
// module 模式:added 为聚合后的未覆盖目录,需人工登记目录条目
|
|
283
|
+
jsonOutput.added = result.added.map(d => ({ dir: d }));
|
|
284
|
+
jsonOutput.removed = result.removed.map(f => ({ file: f }));
|
|
285
|
+
(jsonOutput.resolution as Array<Record<string, unknown>>).push({
|
|
286
|
+
action: 'register-capability-dirs',
|
|
287
|
+
details:
|
|
288
|
+
'在 CAPABILITIES.md 中为这些目录登记一行目录条目(如 `| 模块名 | src/xxx/ | 说明 |`)',
|
|
289
|
+
dirs: result.added,
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
jsonOutput.added = result.added.map(f => {
|
|
293
|
+
const getBasenameLocal = (s: string) => s.split('/').pop()!;
|
|
294
|
+
return {
|
|
295
|
+
file: f,
|
|
296
|
+
module: currentModules.find(m => getBasenameLocal(m.file) === f),
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
jsonOutput.removed = result.removed.map(f => ({ file: f }));
|
|
300
|
+
(jsonOutput.resolution as Array<Record<string, unknown>>).push({
|
|
301
|
+
action: 'sync-capabilities',
|
|
302
|
+
command: 'harness sync-docs',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
250
305
|
}
|
|
251
306
|
|
|
252
307
|
if (hasContextIssues) {
|
|
@@ -282,8 +337,16 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
282
337
|
}
|
|
283
338
|
|
|
284
339
|
if (result.added.length > 0) {
|
|
285
|
-
|
|
286
|
-
|
|
340
|
+
if (capsMode === 'module') {
|
|
341
|
+
console.log(chalk.yellow(`\n📄 CAPABILITIES.md 未登记以下模块(目录):`));
|
|
342
|
+
result.added.forEach(d => console.log(chalk.gray(` + ${d}`)));
|
|
343
|
+
console.log(
|
|
344
|
+
chalk.gray(' 请在 CAPABILITIES.md 中为这些目录登记一行目录条目(如 `| 模块名 | src/xxx/ | 说明 |`)')
|
|
345
|
+
);
|
|
346
|
+
} else {
|
|
347
|
+
console.log(chalk.yellow(`\n📄 CAPABILITIES.md 缺少以下模块:`));
|
|
348
|
+
result.added.forEach(f => console.log(chalk.gray(` + ${f}`)));
|
|
349
|
+
}
|
|
287
350
|
}
|
|
288
351
|
|
|
289
352
|
if (result.removed.length > 0) {
|
|
@@ -337,7 +400,7 @@ export async function syncDocs(options: SyncDocsOptions): Promise<boolean> {
|
|
|
337
400
|
}
|
|
338
401
|
|
|
339
402
|
if (!capsIsCapabilityListing && hasTableIssues) {
|
|
340
|
-
await updateCapabilitiesFile(capabilitiesPath, currentModules, existingFiles, result);
|
|
403
|
+
await updateCapabilitiesFile(capabilitiesPath, currentModules, existingFiles, result, capsMode);
|
|
341
404
|
console.log(chalk.green(`\n✅ 已更新 CAPABILITIES.md`));
|
|
342
405
|
}
|
|
343
406
|
|
|
@@ -68,3 +68,18 @@ export function readCapabilitiesEntries(capabilitiesPath: string, options: Capab
|
|
|
68
68
|
return [];
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 未覆盖文件的聚合目录(module 模式)
|
|
74
|
+
*
|
|
75
|
+
* 规则:取源码根下的第一级子目录(root=src、file=src/core/foo.ts → src/core/);
|
|
76
|
+
* 源码根直接下的文件聚到根(file=src/foo.ts → src/)。
|
|
77
|
+
*/
|
|
78
|
+
export function aggregateToSourceSubdir(root: string, file: string): string {
|
|
79
|
+
if (!root) return file;
|
|
80
|
+
const prefix = root.endsWith('/') ? root : root + '/';
|
|
81
|
+
const rest = file.startsWith(prefix) ? file.slice(prefix.length) : file;
|
|
82
|
+
const idx = rest.indexOf('/');
|
|
83
|
+
if (idx === -1) return prefix;
|
|
84
|
+
return prefix + rest.slice(0, idx) + '/';
|
|
85
|
+
}
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* 两步验证:
|
|
5
5
|
* 1. Git diff 增量 — 当前变更的非测试文件是否在文档中有记录
|
|
6
6
|
* 2. 全量扫描 — 源码根下所有源文件是否都在 CAPABILITIES.md 中(T-058)
|
|
7
|
+
*
|
|
8
|
+
* governance.capabilities.mode=module 时 step 2 改按目录覆盖判定:
|
|
9
|
+
* 文件条目精确匹配或目录条目前缀覆盖,未覆盖文件按源码根下第一级子目录聚合。
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { existsSync } from 'fs';
|
|
10
13
|
import { join } from 'path';
|
|
11
|
-
import { readCapabilitiesEntries } from '../capabilities-parser';
|
|
14
|
+
import { aggregateToSourceSubdir, readCapabilitiesEntries } from '../capabilities-parser';
|
|
15
|
+
import { getCapabilitiesMode } from '../../project-config-loader';
|
|
12
16
|
import { detectSourceRoots } from '../../../utils/detect-source-roots';
|
|
13
17
|
import type { ConstraintCheck } from './types';
|
|
14
18
|
|
|
@@ -49,7 +53,31 @@ export const capabilitySync: ConstraintCheck = {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// ── Step 2: 全量源码根扫描(T-058 修复核心)──
|
|
52
|
-
|
|
56
|
+
const capabilitiesMode = getCapabilitiesMode(projectPath);
|
|
57
|
+
if (capabilitiesMode === 'module') {
|
|
58
|
+
// module 模式:文件条目精确匹配 或 目录条目前缀覆盖;
|
|
59
|
+
// 未覆盖文件按源码根下第一级子目录聚合,任一组非空即失败
|
|
60
|
+
const entries = readCapabilitiesEntries(capabilitiesPath, { includeDirs: true });
|
|
61
|
+
if (entries.length > 0) {
|
|
62
|
+
const fileEntries = entries.filter(e => !e.endsWith('/'));
|
|
63
|
+
const dirEntries = entries.filter(e => e.endsWith('/'));
|
|
64
|
+
const uncoveredDirs = new Set<string>();
|
|
65
|
+
const sourceRoots = detectSourceRoots(projectPath);
|
|
66
|
+
for (const root of sourceRoots) {
|
|
67
|
+
const srcDir = join(projectPath, root);
|
|
68
|
+
if (existsSync(srcDir)) {
|
|
69
|
+
for (const file of env.srcScan(root)) {
|
|
70
|
+
const covered =
|
|
71
|
+
fileEntries.includes(file) || dirEntries.some(d => file.startsWith(d));
|
|
72
|
+
if (!covered) {
|
|
73
|
+
uncoveredDirs.add(aggregateToSourceSubdir(root, file));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (uncoveredDirs.size > 0) return false;
|
|
79
|
+
}
|
|
80
|
+
} else if (listedFiles.length > 0) {
|
|
53
81
|
const sourceRoots = detectSourceRoots(projectPath);
|
|
54
82
|
for (const root of sourceRoots) {
|
|
55
83
|
const srcDir = join(projectPath, root);
|
|
@@ -197,7 +197,7 @@ export const GUIDELINES: Record<string, Constraint> = {
|
|
|
197
197
|
capability_sync: {
|
|
198
198
|
id: 'capability_sync',
|
|
199
199
|
rule: 'CODE CHANGES MUST UPDATE CAPABILITIES.MD',
|
|
200
|
-
message: '
|
|
200
|
+
message: '核心模块变更必须同步功能清单(支持模块级目录条目登记)',
|
|
201
201
|
level: 'guideline',
|
|
202
202
|
trigger: ['module_creation', 'module_modification', 'module_deletion', 'module_extension'],
|
|
203
203
|
enforcement: 'update-capabilities',
|
|
@@ -212,7 +212,9 @@ export const GUIDELINES: Record<string, Constraint> = {
|
|
|
212
212
|
[不强制更新]
|
|
213
213
|
- 内部重构不影响对外接口
|
|
214
214
|
- bug fix 不改变功能
|
|
215
|
-
-
|
|
215
|
+
- 性能优化不改变接口
|
|
216
|
+
|
|
217
|
+
支持模块级目录条目登记:governance.capabilities.mode=module 时按目录覆盖判定(如 src/core/ 一条目录条目覆盖该目录下所有源文件)。`,
|
|
216
218
|
exceptions: ['internal_refactor', 'bug_fix_only', 'performance_optimization'],
|
|
217
219
|
},
|
|
218
220
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ProjectConfig,
|
|
14
14
|
CustomConstraintDefinition,
|
|
15
15
|
MergedConstraintsConfig,
|
|
16
|
+
CapabilitiesConfig,
|
|
16
17
|
} from '../types/project-config';
|
|
17
18
|
import { IRON_LAWS, GUIDELINES, TIPS } from './constraints/definitions';
|
|
18
19
|
|
|
@@ -69,6 +70,29 @@ export function loadRawProjectConfig(projectPath: string): Record<string, unknow
|
|
|
69
70
|
return raw;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* CAPABILITIES.md 登记模式(governance.capabilities.mode)
|
|
75
|
+
*/
|
|
76
|
+
export type CapabilitiesMode = NonNullable<CapabilitiesConfig['mode']>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 读取 governance.capabilities.mode(缺省 'file',向后兼容)
|
|
80
|
+
*
|
|
81
|
+
* 配置缺失/解析失败/取值非法时一律回落 'file'。
|
|
82
|
+
*/
|
|
83
|
+
export function getCapabilitiesMode(projectPath: string): CapabilitiesMode {
|
|
84
|
+
try {
|
|
85
|
+
const raw = loadRawProjectConfig(projectPath);
|
|
86
|
+
const governance = raw?.governance as Record<string, unknown> | undefined;
|
|
87
|
+
const capabilities = governance?.capabilities as Record<string, unknown> | undefined;
|
|
88
|
+
const mode = capabilities?.mode;
|
|
89
|
+
if (mode === 'file' || mode === 'module' || mode === 'listing') return mode;
|
|
90
|
+
} catch {
|
|
91
|
+
// 配置缺失或解析失败,按默认 file 处理
|
|
92
|
+
}
|
|
93
|
+
return 'file';
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
/**
|
|
73
97
|
* 项目配置加载器
|
|
74
98
|
*/
|
|
@@ -60,6 +60,19 @@ export interface ContextFilesConfig {
|
|
|
60
60
|
required_dirs?: string[];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* CAPABILITIES.md 登记模式配置
|
|
65
|
+
*/
|
|
66
|
+
export interface CapabilitiesConfig {
|
|
67
|
+
/**
|
|
68
|
+
* 登记模式:
|
|
69
|
+
* - file:逐文件登记(默认,历史行为)
|
|
70
|
+
* - module:模块级目录条目登记,按目录前缀覆盖判定
|
|
71
|
+
* - listing:能力清单格式(计数行)的显式声明
|
|
72
|
+
*/
|
|
73
|
+
mode?: 'file' | 'module' | 'listing';
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* CHANGELOG 配置
|
|
65
78
|
*/
|
|
@@ -183,6 +196,9 @@ export interface GovernanceConfig {
|
|
|
183
196
|
/** CONTEXT.md 配置 */
|
|
184
197
|
context_files?: ContextFilesConfig;
|
|
185
198
|
|
|
199
|
+
/** CAPABILITIES.md 登记模式配置 */
|
|
200
|
+
capabilities?: CapabilitiesConfig;
|
|
201
|
+
|
|
186
202
|
/** CHANGELOG 配置 */
|
|
187
203
|
changelog?: ChangelogConfig;
|
|
188
204
|
|