@longzai-intelligence-issues/config 0.0.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.
- package/CHANGELOG.md +12 -0
- package/README.md +23 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +6 -0
- package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
- package/lzi-builder.config.ts +14 -0
- package/lzi-bun.config.ts +8 -0
- package/oxlint.config.ts +3 -0
- package/package.json +40 -0
- package/src/__tests__/baseline/render-baseline.renderer.test.ts +142 -0
- package/src/__tests__/scaffold/render-scaffold.renderer.test.ts +137 -0
- package/src/__tests__/schema/lzi-issues-config.schema.test.ts +144 -0
- package/src/__tests__/validation/validate-cross-entries.test.ts +177 -0
- package/src/baseline/render-baseline.renderer.ts +120 -0
- package/src/index.ts +16 -0
- package/src/loader/load-issues-config.utils.ts +180 -0
- package/src/scaffold/render-scaffold.renderer.ts +420 -0
- package/src/schema/lzi-issues-config.schema.ts +277 -0
- package/src/validation/validate-cross-entries.ts +260 -0
- package/tsconfig/.cache/build.tsbuildinfo +1 -0
- package/tsconfig/.cache/node.tsbuildinfo +1 -0
- package/tsconfig/.cache/test.tsbuildinfo +1 -0
- package/tsconfig/app.json +13 -0
- package/tsconfig/build.json +15 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/test.json +15 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lzi-issues 配置 schema 与解析
|
|
3
|
+
*
|
|
4
|
+
* 单一真源 `lzi-issues.config.ts` 的形态定义:zod 守形态、跨条目语义由
|
|
5
|
+
* validate-cross-entries 纯函数承担(apex numbering.config.ts 同款分工),
|
|
6
|
+
* 非法配置在加载即失败。引擎零仓库知识——一切仓库特有信息(scope 注册、扫描
|
|
7
|
+
* 面、门禁正则)只存在于本配置。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as z from 'zod';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 存储策略全集(ADR-0048 多策略):files 为 Local First 默认
|
|
14
|
+
*/
|
|
15
|
+
export const STORAGE_STRATEGIES = ['files', 'sqlite', 'postgres', 'mysql', 'memory'] as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 存储策略类型
|
|
19
|
+
*/
|
|
20
|
+
export type StorageStrategy = (typeof STORAGE_STRATEGIES)[number];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 关闭门禁挂起词默认集(状态行命中任一即拒绝收口——诚实黄灯维持)
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_PENDING_WORDS = [
|
|
26
|
+
'待补跑',
|
|
27
|
+
'待复跑',
|
|
28
|
+
'待低负载',
|
|
29
|
+
'环境不具备',
|
|
30
|
+
'挂起',
|
|
31
|
+
'后补跑',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* audit 覆盖面「形似编号」筛选默认模式
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_NUMBERISH_PATTERN = '^\\d{1,4}[-._]';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* audit 默认审计根(docs 树)
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_AUDIT_ROOTS = ['docs'] as const;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* init 缺省注册表目录
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_REGISTRY_DIR = 'docs/issues';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* sqlite 策略缺省库文件路径(统一收纳 .lzi/issues/ 命名空间)
|
|
51
|
+
*/
|
|
52
|
+
export const DEFAULT_SQLITE_PATH = '.lzi/issues/issues.sqlite';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* scope key 合法形态(kebab-case)
|
|
56
|
+
*/
|
|
57
|
+
export const SCOPE_KEY_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 编号形态(数字字符串,零填充原文)
|
|
61
|
+
*/
|
|
62
|
+
export const NUMBER_PATTERN = /^\d+$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 扩展名形态(如 .md)
|
|
66
|
+
*/
|
|
67
|
+
export const EXTENSION_PATTERN = /^\.[A-Za-z0-9]+$/;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 保留号条目 schema(不回收复用的历史空洞号)
|
|
71
|
+
*/
|
|
72
|
+
const reservedEntrySchema = z.object({
|
|
73
|
+
number: z.string().regex(NUMBER_PATTERN),
|
|
74
|
+
reason: z.string().min(1),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 棘轮基线条目 schema(豁免必须携带 issue 收编指针)
|
|
79
|
+
*/
|
|
80
|
+
export const baselineEntrySchema = z.object({
|
|
81
|
+
file: z.string().min(1),
|
|
82
|
+
rule: z.string().min(1),
|
|
83
|
+
issue: z.string().min(1),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* glob + 理由条目 schema(豁免/排除逐条带理由登记)
|
|
88
|
+
*/
|
|
89
|
+
const globReasonEntrySchema = z.object({
|
|
90
|
+
glob: z.string().min(1),
|
|
91
|
+
reason: z.string().min(1),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* scope 注册条目 schema
|
|
96
|
+
*/
|
|
97
|
+
const scopeSchema = z.object({
|
|
98
|
+
key: z.string().regex(SCOPE_KEY_PATTERN),
|
|
99
|
+
title: z.string().min(1),
|
|
100
|
+
paths: z.array(z.string().min(1)).min(1),
|
|
101
|
+
registryDir: z.string().min(1),
|
|
102
|
+
numbering: z.object({ reserved: z.array(reservedEntrySchema) }).default({ reserved: [] }),
|
|
103
|
+
slugDigitPolicy: z.literal('forbid').optional(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* scope 注册条目(书写形态)
|
|
108
|
+
*/
|
|
109
|
+
export type ScopeEntryInput = z.input<typeof scopeSchema>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* scope 注册条目(解析后形态)
|
|
113
|
+
*/
|
|
114
|
+
export type ScopeEntry = z.output<typeof scopeSchema>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 存储节 schema(字段级默认,section 缺省整体回退 DEFAULT_STORAGE)
|
|
118
|
+
*/
|
|
119
|
+
const storageSchema = z.object({
|
|
120
|
+
strategy: z.enum(STORAGE_STRATEGIES).default('files'),
|
|
121
|
+
sqlitePath: z.string().min(1).default(DEFAULT_SQLITE_PATH),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* lint 节 schema
|
|
126
|
+
*/
|
|
127
|
+
const lintSchema = z.object({
|
|
128
|
+
baseline: z.array(baselineEntrySchema).default([]),
|
|
129
|
+
strictLegacy: z.boolean().default(false),
|
|
130
|
+
rulePacks: z.array(z.string().min(1)).default([]),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 编号豁免条目 schema(存量撞号豁免:精确文件集 + 棘轮状态 + 理由)
|
|
135
|
+
*/
|
|
136
|
+
const allowlistEntrySchema = z.object({
|
|
137
|
+
scopeKey: z.string().min(1),
|
|
138
|
+
number: z.string().regex(NUMBER_PATTERN),
|
|
139
|
+
files: z.array(z.string().min(1)).min(2),
|
|
140
|
+
status: z.enum(['待清偿', '维持现状']),
|
|
141
|
+
reason: z.string().min(1),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* numbering 节 schema
|
|
146
|
+
*/
|
|
147
|
+
const numberingSchema = z.object({
|
|
148
|
+
allowlist: z.array(allowlistEntrySchema).default([]),
|
|
149
|
+
globalExclusions: z.array(globReasonEntrySchema).default([]),
|
|
150
|
+
audit: z
|
|
151
|
+
.object({
|
|
152
|
+
roots: z
|
|
153
|
+
.array(z.string().min(1))
|
|
154
|
+
.min(1)
|
|
155
|
+
.default([...DEFAULT_AUDIT_ROOTS]),
|
|
156
|
+
numberishPattern: z.string().min(1).default(DEFAULT_NUMBERISH_PATTERN),
|
|
157
|
+
pending: z.array(globReasonEntrySchema).default([]),
|
|
158
|
+
})
|
|
159
|
+
.default({
|
|
160
|
+
roots: [...DEFAULT_AUDIT_ROOTS],
|
|
161
|
+
numberishPattern: DEFAULT_NUMBERISH_PATTERN,
|
|
162
|
+
pending: [],
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* docs 节 schema(scanRoots 必填——引用检查扫描面不设隐式默认)
|
|
168
|
+
*/
|
|
169
|
+
const docsSchema = z.object({
|
|
170
|
+
scanRoots: z.array(z.string().min(1)).min(1),
|
|
171
|
+
includeExtensions: z.array(z.string().regex(EXTENSION_PATTERN)).default(['.md']),
|
|
172
|
+
exclusions: z.array(globReasonEntrySchema).default([]),
|
|
173
|
+
baseline: z.array(baselineEntrySchema).default([]),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* close 节 schema
|
|
178
|
+
*/
|
|
179
|
+
const closeSchema = z.object({
|
|
180
|
+
pendingWords: z.array(z.string().min(1)).default([...DEFAULT_PENDING_WORDS]),
|
|
181
|
+
heavyPattern: z.string().min(1).optional(),
|
|
182
|
+
fullPattern: z.string().min(1).optional(),
|
|
183
|
+
blockingMustBeGreen: z.boolean().default(true),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* prompts 节 schema
|
|
188
|
+
*/
|
|
189
|
+
const promptsSchema = z.object({
|
|
190
|
+
builtin: z.boolean().default(true),
|
|
191
|
+
extraDir: z.string().min(1).optional(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* lzi-issues 配置根 schema
|
|
196
|
+
*
|
|
197
|
+
* docs 节整体可选(未配置时 docs 引用检查命令显式拒绝,不设隐式扫描面);
|
|
198
|
+
* 其余节缺省时整体回退该节全默认值。
|
|
199
|
+
*/
|
|
200
|
+
export const lziIssuesConfigSchema = z.object({
|
|
201
|
+
schemaVersion: z.literal(1),
|
|
202
|
+
storage: storageSchema.default({ strategy: 'files', sqlitePath: DEFAULT_SQLITE_PATH }),
|
|
203
|
+
scopes: z.array(scopeSchema).min(1),
|
|
204
|
+
lint: lintSchema.default({ baseline: [], strictLegacy: false, rulePacks: [] }),
|
|
205
|
+
numbering: numberingSchema.default({
|
|
206
|
+
allowlist: [],
|
|
207
|
+
globalExclusions: [],
|
|
208
|
+
audit: {
|
|
209
|
+
roots: [...DEFAULT_AUDIT_ROOTS],
|
|
210
|
+
numberishPattern: DEFAULT_NUMBERISH_PATTERN,
|
|
211
|
+
pending: [],
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
slugDigitAllowlist: z.array(globReasonEntrySchema).default([]),
|
|
215
|
+
docs: docsSchema.optional(),
|
|
216
|
+
close: closeSchema.default({
|
|
217
|
+
pendingWords: [...DEFAULT_PENDING_WORDS],
|
|
218
|
+
blockingMustBeGreen: true,
|
|
219
|
+
}),
|
|
220
|
+
prompts: promptsSchema.default({ builtin: true }),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* lzi-issues 配置(用户书写形态,可省略各默认值)
|
|
225
|
+
*/
|
|
226
|
+
export type LziIssuesConfigInput = z.input<typeof lziIssuesConfigSchema>;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* lzi-issues 配置(解析后形态,默认值全部落定)
|
|
230
|
+
*/
|
|
231
|
+
export type LziIssuesConfig = z.output<typeof lziIssuesConfigSchema>;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 棘轮基线条目
|
|
235
|
+
*/
|
|
236
|
+
export type LintBaselineEntry = z.infer<typeof baselineEntrySchema>;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 解析并校验配置(非法即抛,加载即失败)
|
|
240
|
+
*
|
|
241
|
+
* @param input - 配置原始对象(defineConfig 产物或统一配置字段值)
|
|
242
|
+
* @returns 默认值落定的配置
|
|
243
|
+
* @throws {@link Error} 形态违规(消息聚合全部违规项;跨条目语义校验由
|
|
244
|
+
* validateLziIssuesConfig 在加载链路上追加执行)
|
|
245
|
+
*/
|
|
246
|
+
export function parseLziIssuesConfig(input: unknown): LziIssuesConfig {
|
|
247
|
+
/**
|
|
248
|
+
* 形态解析结果
|
|
249
|
+
*/
|
|
250
|
+
const parsed = lziIssuesConfigSchema.safeParse(input);
|
|
251
|
+
|
|
252
|
+
if (!parsed.success) {
|
|
253
|
+
/**
|
|
254
|
+
* 形态违规明细(路径 + 消息)
|
|
255
|
+
*/
|
|
256
|
+
const details = parsed.error.issues
|
|
257
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
258
|
+
.join(';');
|
|
259
|
+
|
|
260
|
+
throw new Error(`lzi-issues 配置形态非法:${details}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return parsed.data;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 定义 lzi-issues 配置(identity 工厂)
|
|
268
|
+
*
|
|
269
|
+
* 类型透传,与 lzi-dev.config.ts / oxfmt.config.ts 等 workspace 配置惯例同构:
|
|
270
|
+
* 不在工厂层做任何合并——默认值由本模块 schema 解析层统一落定。
|
|
271
|
+
*
|
|
272
|
+
* @param config - 配置对象(可省略各默认值)
|
|
273
|
+
* @returns 原样返回传入的配置对象
|
|
274
|
+
*/
|
|
275
|
+
export function defineConfig(config: LziIssuesConfigInput): LziIssuesConfigInput {
|
|
276
|
+
return config;
|
|
277
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lzi-issues 配置跨条目语义校验(纯函数)
|
|
3
|
+
*
|
|
4
|
+
* zod 只守形态;跨条目语义(查重、指向、路径形态)集中于此,非法即聚合抛错
|
|
5
|
+
* (apex numbering.config.ts 的 validateCrossEntries 同款分工)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LziIssuesConfig } from '@/schema/lzi-issues-config.schema';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 跨条目校验违规消息列表类型
|
|
12
|
+
*/
|
|
13
|
+
export type CrossEntryViolations = string[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 校验单条相对路径形态(禁绝对路径、禁 .. 上溯、禁反斜杠)
|
|
17
|
+
*
|
|
18
|
+
* @param label - 违规消息中的条目标签(如「scopes[0].registryDir」)
|
|
19
|
+
* @param value - 待校验路径
|
|
20
|
+
* @param violations - 违规消息收集器
|
|
21
|
+
*/
|
|
22
|
+
function checkRelativePath(label: string, value: string, violations: CrossEntryViolations): void {
|
|
23
|
+
if (value.startsWith('/') || value.startsWith('\\')) {
|
|
24
|
+
violations.push(`${label}「${value}」不得为绝对路径`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (value.includes('\\')) {
|
|
28
|
+
violations.push(`${label}「${value}」不得包含反斜杠(须为 posix 相对路径)`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (value.split('/').includes('..')) {
|
|
32
|
+
violations.push(`${label}「${value}」不得包含 .. 上溯段`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 校验 glob 基本形态(禁反斜杠、禁绝对路径起首;完整可编译性由引擎 mini-glob
|
|
38
|
+
* 在加载链路上首编译时把关——本包保持零引擎依赖)
|
|
39
|
+
*
|
|
40
|
+
* @param label - 违规消息中的条目标签
|
|
41
|
+
* @param glob - 待校验 glob
|
|
42
|
+
* @param violations - 违规消息收集器
|
|
43
|
+
*/
|
|
44
|
+
function checkGlobShape(label: string, glob: string, violations: CrossEntryViolations): void {
|
|
45
|
+
if (glob.includes('\\')) {
|
|
46
|
+
violations.push(`${label}「${glob}」不得包含反斜杠`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (glob.startsWith('/')) {
|
|
50
|
+
violations.push(`${label}「${glob}」不得以 / 起首(须为仓库相对 glob)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 校验 glob 列表内部查重
|
|
56
|
+
*
|
|
57
|
+
* @param label - 列表标签
|
|
58
|
+
* @param globs - glob 值列表
|
|
59
|
+
* @param violations - 违规消息收集器
|
|
60
|
+
*/
|
|
61
|
+
function checkGlobUniqueness(
|
|
62
|
+
label: string,
|
|
63
|
+
globs: string[],
|
|
64
|
+
violations: CrossEntryViolations,
|
|
65
|
+
): void {
|
|
66
|
+
/**
|
|
67
|
+
* 已见 glob 集合
|
|
68
|
+
*/
|
|
69
|
+
const seen = new Set<string>();
|
|
70
|
+
|
|
71
|
+
for (const glob of globs) {
|
|
72
|
+
if (seen.has(glob)) {
|
|
73
|
+
violations.push(`${label} 存在重复 glob「${glob}」`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
seen.add(glob);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 跨条目语义校验
|
|
82
|
+
*
|
|
83
|
+
* 校验面:scope key 查重、保留号逐 scope 查重、基线 (file,rule) 查重(lint 与
|
|
84
|
+
* docs 各自为界)、allowlist 指向已登记 scope 且条目查重、全部 glob 形态与
|
|
85
|
+
* 查重、audit.roots 与 registryDir 相对路径形态。
|
|
86
|
+
*
|
|
87
|
+
* @param config - 解析后配置
|
|
88
|
+
* @returns 违规消息列表(空列表 = 合法)
|
|
89
|
+
*/
|
|
90
|
+
export function validateCrossEntries(config: LziIssuesConfig): CrossEntryViolations {
|
|
91
|
+
/**
|
|
92
|
+
* 违规消息收集器
|
|
93
|
+
*/
|
|
94
|
+
const violations: CrossEntryViolations = [];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 已见 scope key 集合
|
|
98
|
+
*/
|
|
99
|
+
const scopeKeys = new Set<string>();
|
|
100
|
+
|
|
101
|
+
config.scopes.forEach((scope, index) => {
|
|
102
|
+
/**
|
|
103
|
+
* scope 条目标签
|
|
104
|
+
*/
|
|
105
|
+
const label = `scopes[${index}](${scope.key})`;
|
|
106
|
+
|
|
107
|
+
if (scopeKeys.has(scope.key)) {
|
|
108
|
+
violations.push(`scope key「${scope.key}」重复注册`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
scopeKeys.add(scope.key);
|
|
112
|
+
|
|
113
|
+
checkRelativePath(`${label}.registryDir`, scope.registryDir, violations);
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 已见保留号集合(逐 scope 命名空间)
|
|
117
|
+
*/
|
|
118
|
+
const reservedNumbers = new Set<string>();
|
|
119
|
+
|
|
120
|
+
for (const entry of scope.numbering.reserved) {
|
|
121
|
+
if (reservedNumbers.has(entry.number)) {
|
|
122
|
+
violations.push(`${label} 保留号「${entry.number}」重复登记`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
reservedNumbers.add(entry.number);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* lint 基线 (file,rule) 查重
|
|
131
|
+
*/
|
|
132
|
+
const lintBaselinePairs = new Set<string>();
|
|
133
|
+
|
|
134
|
+
for (const entry of config.lint.baseline) {
|
|
135
|
+
/**
|
|
136
|
+
* 基线条目配对键
|
|
137
|
+
*/
|
|
138
|
+
const pair = `${entry.file}::${entry.rule}`;
|
|
139
|
+
|
|
140
|
+
if (lintBaselinePairs.has(pair)) {
|
|
141
|
+
violations.push(`lint.baseline 条目(${entry.file}, ${entry.rule})重复`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lintBaselinePairs.add(pair);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (config.docs !== undefined) {
|
|
148
|
+
/**
|
|
149
|
+
* docs 基线 (file,rule) 查重
|
|
150
|
+
*/
|
|
151
|
+
const docsBaselinePairs = new Set<string>();
|
|
152
|
+
|
|
153
|
+
for (const entry of config.docs.baseline) {
|
|
154
|
+
/**
|
|
155
|
+
* 基线条目配对键
|
|
156
|
+
*/
|
|
157
|
+
const pair = `${entry.file}::${entry.rule}`;
|
|
158
|
+
|
|
159
|
+
if (docsBaselinePairs.has(pair)) {
|
|
160
|
+
violations.push(`docs.baseline 条目(${entry.file}, ${entry.rule})重复`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
docsBaselinePairs.add(pair);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const exclusion of config.docs.exclusions) {
|
|
167
|
+
checkGlobShape(`docs.exclusions`, exclusion.glob, violations);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
checkGlobUniqueness(
|
|
171
|
+
'docs.exclusions',
|
|
172
|
+
config.docs.exclusions.map((e) => e.glob),
|
|
173
|
+
violations,
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
for (const root of config.docs.scanRoots) {
|
|
177
|
+
checkRelativePath('docs.scanRoots', root, violations);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* allowlist 指向与条目查重
|
|
183
|
+
*/
|
|
184
|
+
const allowlistKeys = new Set<string>();
|
|
185
|
+
|
|
186
|
+
for (const entry of config.numbering.allowlist) {
|
|
187
|
+
if (!scopeKeys.has(entry.scopeKey)) {
|
|
188
|
+
violations.push(
|
|
189
|
+
`numbering.allowlist 条目指向未登记 scope「${entry.scopeKey}」(编号 ${entry.number})`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 豁免条目唯一键(scopeKey + number + 排序文件集)
|
|
195
|
+
*/
|
|
196
|
+
const key = `${entry.scopeKey}::${entry.number}::${[...entry.files].sort().join(',')}`;
|
|
197
|
+
|
|
198
|
+
if (allowlistKeys.has(key)) {
|
|
199
|
+
violations.push(`numbering.allowlist 条目(${entry.scopeKey} ${entry.number})重复`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
allowlistKeys.add(key);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
for (const exclusion of config.numbering.globalExclusions) {
|
|
206
|
+
checkGlobShape('numbering.globalExclusions', exclusion.glob, violations);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
checkGlobUniqueness(
|
|
210
|
+
'numbering.globalExclusions',
|
|
211
|
+
config.numbering.globalExclusions.map((e) => e.glob),
|
|
212
|
+
violations,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
for (const pending of config.numbering.audit.pending) {
|
|
216
|
+
checkGlobShape('numbering.audit.pending', pending.glob, violations);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
checkGlobUniqueness(
|
|
220
|
+
'numbering.audit.pending',
|
|
221
|
+
config.numbering.audit.pending.map((e) => e.glob),
|
|
222
|
+
violations,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
for (const root of config.numbering.audit.roots) {
|
|
226
|
+
checkRelativePath('numbering.audit.roots', root, violations);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (const entry of config.slugDigitAllowlist) {
|
|
230
|
+
checkGlobShape('slugDigitAllowlist', entry.glob, violations);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
checkGlobUniqueness(
|
|
234
|
+
'slugDigitAllowlist',
|
|
235
|
+
config.slugDigitAllowlist.map((e) => e.glob),
|
|
236
|
+
violations,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return violations;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 校验配置并聚合抛错(加载链路统一入口)
|
|
244
|
+
*
|
|
245
|
+
* @param config - 解析后配置
|
|
246
|
+
* @returns 原样返回合法配置(链式便利)
|
|
247
|
+
* @throws {@link Error} 存在任何跨条目违规(消息聚合全部违规项)
|
|
248
|
+
*/
|
|
249
|
+
export function validateLziIssuesConfig(config: LziIssuesConfig): LziIssuesConfig {
|
|
250
|
+
/**
|
|
251
|
+
* 违规消息列表
|
|
252
|
+
*/
|
|
253
|
+
const violations = validateCrossEntries(config);
|
|
254
|
+
|
|
255
|
+
if (violations.length > 0) {
|
|
256
|
+
throw new Error(`lzi-issues 配置跨条目校验失败:${violations.join(';')}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return config;
|
|
260
|
+
}
|