@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,137 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
appendScopeToConfigText,
|
|
8
|
+
renderConfigScaffoldFile,
|
|
9
|
+
} from '@/scaffold/render-scaffold.renderer';
|
|
10
|
+
import { parseLziIssuesConfig } from '@/schema/lzi-issues-config.schema';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 动态导入序号(每次唯一文件名,规避 bun 模块 URL 缓存返回陈旧模块)
|
|
14
|
+
*/
|
|
15
|
+
let importSequence = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 动态导入生成的配置文本(改写包导入为本包内路径后落盘导入并解析)
|
|
19
|
+
*
|
|
20
|
+
* @param generated - 生成的配置文件全文
|
|
21
|
+
* @returns 解析后配置
|
|
22
|
+
*/
|
|
23
|
+
async function importAndParse(generated: string) {
|
|
24
|
+
/**
|
|
25
|
+
* 临时目录(包内,保证模块解析可用)
|
|
26
|
+
*/
|
|
27
|
+
const tmpDir = join(import.meta.dir, '__tmp-scaffold__');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 临时文件路径(序号唯一化)
|
|
31
|
+
*/
|
|
32
|
+
const tmpFile = join(tmpDir, `gen-${(importSequence += 1)}.config.ts`);
|
|
33
|
+
|
|
34
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
/**
|
|
38
|
+
* 可导入文本(包导入改写为包内别名,规避 dist 构建依赖)
|
|
39
|
+
*/
|
|
40
|
+
const importable = generated.replace(
|
|
41
|
+
"'@longzai-intelligence-issues/config'",
|
|
42
|
+
"'@/schema/lzi-issues-config.schema'",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
writeFileSync(tmpFile, importable);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 动态导入的配置模块
|
|
49
|
+
*/
|
|
50
|
+
const module = (await import(pathToFileURL(tmpFile).href)) as { default: unknown };
|
|
51
|
+
|
|
52
|
+
return parseLziIssuesConfig(module.default);
|
|
53
|
+
} finally {
|
|
54
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('renderConfigScaffoldFile', () => {
|
|
59
|
+
test('生成文件含契约头、defineConfig 与缺省 scope 值', () => {
|
|
60
|
+
const text = renderConfigScaffoldFile({ key: 'demo', title: '演示域' });
|
|
61
|
+
|
|
62
|
+
expect(
|
|
63
|
+
text.includes("import { defineConfig } from '@longzai-intelligence-issues/config';"),
|
|
64
|
+
).toBe(true);
|
|
65
|
+
expect(text.includes('lzi-issues init')).toBe(true);
|
|
66
|
+
expect(text.includes("key: 'demo'")).toBe(true);
|
|
67
|
+
expect(text.includes("paths: ['.']")).toBe(true);
|
|
68
|
+
expect(text.includes("registryDir: 'docs/issues'")).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('生成文件回环:动态导入后解析合法', async () => {
|
|
72
|
+
const config = await importAndParse(renderConfigScaffoldFile({ key: 'demo', title: '演示域' }));
|
|
73
|
+
|
|
74
|
+
expect(config.scopes).toHaveLength(1);
|
|
75
|
+
expect(config.scopes[0]?.key).toBe('demo');
|
|
76
|
+
expect(config.scopes[0]?.paths).toEqual(['.']);
|
|
77
|
+
expect(config.storage.strategy).toBe('files');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('appendScopeToConfigText', () => {
|
|
82
|
+
test('追加成功:两 scope 在场且回环解析合法', async () => {
|
|
83
|
+
/**
|
|
84
|
+
* 追加后文本
|
|
85
|
+
*/
|
|
86
|
+
const appended = appendScopeToConfigText(
|
|
87
|
+
renderConfigScaffoldFile({ key: 'demo', title: '演示域' }),
|
|
88
|
+
{ key: 'domains', title: '领域层', paths: ['domains/**'], registryDir: 'docs/domain-issues' },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(appended.includes("key: 'demo'")).toBe(true);
|
|
92
|
+
expect(appended.includes("key: 'domains'")).toBe(true);
|
|
93
|
+
|
|
94
|
+
const config = await importAndParse(appended);
|
|
95
|
+
|
|
96
|
+
expect(config.scopes.map((scope) => scope.key)).toEqual(['demo', 'domains']);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('重复 key 显式抛错', () => {
|
|
100
|
+
/**
|
|
101
|
+
* 起步文本
|
|
102
|
+
*/
|
|
103
|
+
const base = renderConfigScaffoldFile({ key: 'demo', title: '演示域' });
|
|
104
|
+
|
|
105
|
+
expect(() => appendScopeToConfigText(base, { key: 'demo', title: '再注册' })).toThrow('已注册');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('非规范缩进的手写形态同样可追加且回环合法', async () => {
|
|
109
|
+
/**
|
|
110
|
+
* 手写形态(紧凑数组、双引号、注释干扰)
|
|
111
|
+
*/
|
|
112
|
+
const handwritten = [
|
|
113
|
+
"import { defineConfig } from '@longzai-intelligence-issues/config';",
|
|
114
|
+
'',
|
|
115
|
+
'/** 手写配置(含注释与字符串中的括号 [ ] 干扰) */',
|
|
116
|
+
'export default defineConfig({',
|
|
117
|
+
' schemaVersion: 1,',
|
|
118
|
+
" scopes: [{ key: 'root', title: '全局', paths: ['.'], registryDir: 'docs/issues' }],",
|
|
119
|
+
'});',
|
|
120
|
+
].join('\n');
|
|
121
|
+
|
|
122
|
+
const appended = appendScopeToConfigText(handwritten, { key: 'demo', title: '演示域' });
|
|
123
|
+
|
|
124
|
+
const config = await importAndParse(appended);
|
|
125
|
+
|
|
126
|
+
expect(config.scopes.map((scope) => scope.key)).toEqual(['root', 'demo']);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('缺 scopes 数组显式抛错', () => {
|
|
130
|
+
expect(() =>
|
|
131
|
+
appendScopeToConfigText('export default defineConfig({ schemaVersion: 1 });', {
|
|
132
|
+
key: 'demo',
|
|
133
|
+
title: 'x',
|
|
134
|
+
}),
|
|
135
|
+
).toThrow('缺少 scopes 数组');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_PENDING_WORDS,
|
|
5
|
+
DEFAULT_SQLITE_PATH,
|
|
6
|
+
parseLziIssuesConfig,
|
|
7
|
+
} from '@/schema/lzi-issues-config.schema';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 最小合法配置(仅必填项)
|
|
11
|
+
*/
|
|
12
|
+
const MINIMAL_INPUT = {
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
scopes: [
|
|
15
|
+
{
|
|
16
|
+
key: 'root',
|
|
17
|
+
title: '项目全局',
|
|
18
|
+
paths: ['.'],
|
|
19
|
+
registryDir: 'docs/issues',
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('parseLziIssuesConfig 默认值落定', () => {
|
|
25
|
+
test('最小配置补全全部默认值', () => {
|
|
26
|
+
const config = parseLziIssuesConfig(MINIMAL_INPUT);
|
|
27
|
+
|
|
28
|
+
expect(config.storage.strategy).toBe('files');
|
|
29
|
+
expect(config.storage.sqlitePath).toBe(DEFAULT_SQLITE_PATH);
|
|
30
|
+
expect(config.lint).toEqual({ baseline: [], strictLegacy: false, rulePacks: [] });
|
|
31
|
+
expect(config.numbering.audit.roots).toEqual(['docs']);
|
|
32
|
+
expect(config.numbering.allowlist).toEqual([]);
|
|
33
|
+
expect(config.slugDigitAllowlist).toEqual([]);
|
|
34
|
+
expect(config.docs).toBeUndefined();
|
|
35
|
+
expect(config.close.pendingWords).toEqual([...DEFAULT_PENDING_WORDS]);
|
|
36
|
+
expect(config.close.blockingMustBeGreen).toBe(true);
|
|
37
|
+
expect(config.prompts.builtin).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('scope.numbering 缺省为空保留号集', () => {
|
|
41
|
+
const config = parseLziIssuesConfig(MINIMAL_INPUT);
|
|
42
|
+
|
|
43
|
+
expect(config.scopes[0]?.numbering).toEqual({ reserved: [] });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('显式值覆盖默认值', () => {
|
|
47
|
+
const config = parseLziIssuesConfig({
|
|
48
|
+
...MINIMAL_INPUT,
|
|
49
|
+
storage: { strategy: 'sqlite', sqlitePath: 'custom.sqlite' },
|
|
50
|
+
close: { blockingMustBeGreen: false },
|
|
51
|
+
scopes: [
|
|
52
|
+
{
|
|
53
|
+
key: 'root',
|
|
54
|
+
title: '项目全局',
|
|
55
|
+
paths: ['packages/**'],
|
|
56
|
+
registryDir: 'docs/issues',
|
|
57
|
+
numbering: { reserved: [{ number: '0024', reason: '历史空洞不回收' }] },
|
|
58
|
+
slugDigitPolicy: 'forbid',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(config.storage.strategy).toBe('sqlite');
|
|
64
|
+
expect(config.storage.sqlitePath).toBe('custom.sqlite');
|
|
65
|
+
expect(config.close.blockingMustBeGreen).toBe(false);
|
|
66
|
+
expect(config.scopes[0]?.numbering.reserved).toEqual([
|
|
67
|
+
{ number: '0024', reason: '历史空洞不回收' },
|
|
68
|
+
]);
|
|
69
|
+
expect(config.scopes[0]?.slugDigitPolicy).toBe('forbid');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('docs 节可省略,登记时 scanRoots 必填', () => {
|
|
73
|
+
const withDocs = parseLziIssuesConfig({
|
|
74
|
+
...MINIMAL_INPUT,
|
|
75
|
+
docs: { scanRoots: ['docs'] },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(withDocs.docs?.scanRoots).toEqual(['docs']);
|
|
79
|
+
expect(withDocs.docs?.includeExtensions).toEqual(['.md']);
|
|
80
|
+
|
|
81
|
+
expect(() =>
|
|
82
|
+
parseLziIssuesConfig({
|
|
83
|
+
...MINIMAL_INPUT,
|
|
84
|
+
docs: { scanRoots: [] },
|
|
85
|
+
}),
|
|
86
|
+
).toThrow('docs');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('parseLziIssuesConfig 形态拒收', () => {
|
|
91
|
+
test('schemaVersion 非字面量 1 拒收', () => {
|
|
92
|
+
expect(() => parseLziIssuesConfig({ ...MINIMAL_INPUT, schemaVersion: 2 })).toThrow(
|
|
93
|
+
'schemaVersion',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('scope key 非 kebab-case 拒收', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
parseLziIssuesConfig({
|
|
100
|
+
...MINIMAL_INPUT,
|
|
101
|
+
scopes: [{ key: 'Bad_Key', title: 'x', paths: ['.'], registryDir: 'docs/issues' }],
|
|
102
|
+
}),
|
|
103
|
+
).toThrow('key');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('存储策略非法值拒收', () => {
|
|
107
|
+
expect(() =>
|
|
108
|
+
parseLziIssuesConfig({
|
|
109
|
+
...MINIMAL_INPUT,
|
|
110
|
+
storage: { strategy: 'mongo' },
|
|
111
|
+
}),
|
|
112
|
+
).toThrow('strategy');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('保留号非数字拒收', () => {
|
|
116
|
+
expect(() =>
|
|
117
|
+
parseLziIssuesConfig({
|
|
118
|
+
...MINIMAL_INPUT,
|
|
119
|
+
scopes: [
|
|
120
|
+
{
|
|
121
|
+
key: 'root',
|
|
122
|
+
title: 'x',
|
|
123
|
+
paths: ['.'],
|
|
124
|
+
registryDir: 'docs/issues',
|
|
125
|
+
numbering: { reserved: [{ number: 'abc', reason: 'r' }] },
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
}),
|
|
129
|
+
).toThrow('number');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('豁免条目文件集少于 2 拒收', () => {
|
|
133
|
+
expect(() =>
|
|
134
|
+
parseLziIssuesConfig({
|
|
135
|
+
...MINIMAL_INPUT,
|
|
136
|
+
numbering: {
|
|
137
|
+
allowlist: [
|
|
138
|
+
{ scopeKey: 'root', number: '0001', files: ['a.md'], status: '待清偿', reason: 'r' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
).toThrow('files');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { parseLziIssuesConfig } from '@/schema/lzi-issues-config.schema';
|
|
4
|
+
import { validateCrossEntries } from '@/validation/validate-cross-entries';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 合法基准配置(跨条目校验用)
|
|
8
|
+
*/
|
|
9
|
+
const BASE_INPUT = {
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
scopes: [
|
|
12
|
+
{ key: 'root', title: '全局', paths: ['.'], registryDir: 'docs/issues' },
|
|
13
|
+
{
|
|
14
|
+
key: 'domains',
|
|
15
|
+
title: '领域层',
|
|
16
|
+
paths: ['domains/**'],
|
|
17
|
+
registryDir: 'docs/domain-issues',
|
|
18
|
+
numbering: { reserved: [{ number: '0081', reason: '历史空洞不回收' }] },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
docs: { scanRoots: ['docs'], exclusions: [{ glob: 'docs/generated/**', reason: '生成物' }] },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 解析基准配置(形态合法,跨条目校验前)
|
|
26
|
+
*/
|
|
27
|
+
const parseBase = (overrides: Record<string, unknown>) =>
|
|
28
|
+
parseLziIssuesConfig({ ...BASE_INPUT, ...overrides });
|
|
29
|
+
|
|
30
|
+
describe('validateCrossEntries', () => {
|
|
31
|
+
test('合法配置零违规', () => {
|
|
32
|
+
expect(validateCrossEntries(parseBase({}))).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('scope key 重复注册违规', () => {
|
|
36
|
+
const config = parseBase({
|
|
37
|
+
scopes: [
|
|
38
|
+
{ key: 'root', title: 'a', paths: ['.'], registryDir: 'docs/issues' },
|
|
39
|
+
{ key: 'root', title: 'b', paths: ['.'], registryDir: 'docs/other' },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const violations = validateCrossEntries(config);
|
|
44
|
+
|
|
45
|
+
expect(violations.some((v) => v.includes('root') && v.includes('重复'))).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('保留号逐 scope 查重违规', () => {
|
|
49
|
+
const config = parseBase({
|
|
50
|
+
scopes: [
|
|
51
|
+
{
|
|
52
|
+
key: 'root',
|
|
53
|
+
title: '全局',
|
|
54
|
+
paths: ['.'],
|
|
55
|
+
registryDir: 'docs/issues',
|
|
56
|
+
numbering: {
|
|
57
|
+
reserved: [
|
|
58
|
+
{ number: '0024', reason: 'a' },
|
|
59
|
+
{ number: '0024', reason: 'b' },
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(validateCrossEntries(config).some((v) => v.includes('0024') && v.includes('重复'))).toBe(
|
|
67
|
+
true,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('lint 基线 (file,rule) 重复违规', () => {
|
|
72
|
+
const config = parseBase({
|
|
73
|
+
lint: {
|
|
74
|
+
baseline: [
|
|
75
|
+
{ file: 'docs/issues/0001-a.md', rule: 'x', issue: '0002' },
|
|
76
|
+
{ file: 'docs/issues/0001-a.md', rule: 'x', issue: '0003' },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(validateCrossEntries(config).some((v) => v.includes('lint.baseline'))).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('docs 基线 (file,rule) 重复违规', () => {
|
|
85
|
+
const config = parseBase({
|
|
86
|
+
docs: {
|
|
87
|
+
scanRoots: ['docs'],
|
|
88
|
+
baseline: [
|
|
89
|
+
{ file: 'README.md', rule: 'doc-ref-unknown', issue: '0001' },
|
|
90
|
+
{ file: 'README.md', rule: 'doc-ref-unknown', issue: '0002' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(validateCrossEntries(config).some((v) => v.includes('docs.baseline'))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('allowlist 指向未登记 scope 违规', () => {
|
|
99
|
+
const config = parseBase({
|
|
100
|
+
numbering: {
|
|
101
|
+
allowlist: [
|
|
102
|
+
{
|
|
103
|
+
scopeKey: 'ghost',
|
|
104
|
+
number: '0001',
|
|
105
|
+
files: ['a.md', 'b.md'],
|
|
106
|
+
status: '维持现状',
|
|
107
|
+
reason: 'r',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(
|
|
114
|
+
validateCrossEntries(config).some((v) => v.includes('ghost') && v.includes('未登记')),
|
|
115
|
+
).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('allowlist 条目(排序文件集一致即判重)违规', () => {
|
|
119
|
+
const config = parseBase({
|
|
120
|
+
numbering: {
|
|
121
|
+
allowlist: [
|
|
122
|
+
{
|
|
123
|
+
scopeKey: 'root',
|
|
124
|
+
number: '0053',
|
|
125
|
+
files: ['a.md', 'b.md'],
|
|
126
|
+
status: '待清偿',
|
|
127
|
+
reason: 'r',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
scopeKey: 'root',
|
|
131
|
+
number: '0053',
|
|
132
|
+
files: ['b.md', 'a.md'],
|
|
133
|
+
status: '待清偿',
|
|
134
|
+
reason: 'r',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
validateCrossEntries(config).some((v) => v.includes('allowlist') && v.includes('重复')),
|
|
142
|
+
).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('glob 反斜杠与绝对起首违规', () => {
|
|
146
|
+
const config = parseBase({
|
|
147
|
+
slugDigitAllowlist: [{ glob: 'docs\\win\\**', reason: 'r' }],
|
|
148
|
+
numbering: { globalExclusions: [{ glob: '/abs/**', reason: 'r' }] },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const violations = validateCrossEntries(config);
|
|
152
|
+
|
|
153
|
+
expect(violations.some((v) => v.includes('反斜杠'))).toBe(true);
|
|
154
|
+
expect(violations.some((v) => v.includes('不得以 / 起首'))).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('audit.roots 绝对路径与 .. 上溯违规', () => {
|
|
158
|
+
const config = parseBase({
|
|
159
|
+
numbering: { audit: { roots: ['/docs', 'docs/../etc'] } },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const violations = validateCrossEntries(config);
|
|
163
|
+
|
|
164
|
+
expect(violations.some((v) => v.includes('audit.roots') && v.includes('绝对路径'))).toBe(true);
|
|
165
|
+
expect(violations.some((v) => v.includes('audit.roots') && v.includes('..'))).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('registryDir 绝对路径违规', () => {
|
|
169
|
+
const config = parseBase({
|
|
170
|
+
scopes: [{ key: 'root', title: 'a', paths: ['.'], registryDir: '/docs/issues' }],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(
|
|
174
|
+
validateCrossEntries(config).some((v) => v.includes('registryDir') && v.includes('绝对路径')),
|
|
175
|
+
).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 棘轮基线再生渲染
|
|
3
|
+
*
|
|
4
|
+
* `lint --write-baseline --issue <指针>` / `docs check --write-baseline --issue <指针>`
|
|
5
|
+
* 的落盘载体:以解析值重建配置文件(值保留、基线节替换为再生条目),带棘轮治理
|
|
6
|
+
* 契约注释头。语义边界:再生只许缩短——存在基线外新增违规时由命令层先行拒绝,
|
|
7
|
+
* 本模块只负责渲染。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { LintBaselineEntry, LziIssuesConfig } from '@/schema/lzi-issues-config.schema';
|
|
11
|
+
|
|
12
|
+
import { renderConfigFile } from '@/scaffold/render-scaffold.renderer';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 棘轮契约注释头(再生文件头)
|
|
16
|
+
*/
|
|
17
|
+
export const RATCHET_CONTRACT_HEADER = [
|
|
18
|
+
'/**',
|
|
19
|
+
' * lzi-issues 配置(单一真源)——由 `--write-baseline --issue <指针>` 再生。',
|
|
20
|
+
' *',
|
|
21
|
+
' * 棘轮基线语义:基线按 (file, rule) 精确命中豁免存量违规;基线外新增违规仍红;',
|
|
22
|
+
' * 基线条目无对应现行违规计陈旧红——修复存量即须同步删除对应条目,棘轮只许缩短直至清零;',
|
|
23
|
+
' * 每条豁免必须携带 issue 收编指针(非空)。',
|
|
24
|
+
' *',
|
|
25
|
+
' * 注意:再生以解析值重建本文件,节内手工注释不保留——长篇说明请写入 docs/guides。',
|
|
26
|
+
' */',
|
|
27
|
+
].join('\n');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 基线再生目标节
|
|
31
|
+
*/
|
|
32
|
+
export type BaselineSection = 'lint' | 'docs';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 基线条目排序与去重((file, rule) 唯一,file 升序、rule 次序)
|
|
36
|
+
*
|
|
37
|
+
* @param entries - 原始条目列表
|
|
38
|
+
* @returns 排序去重后的条目列表
|
|
39
|
+
*/
|
|
40
|
+
export function deriveBaselineEntries(entries: readonly LintBaselineEntry[]): LintBaselineEntry[] {
|
|
41
|
+
/**
|
|
42
|
+
* (file, rule) 唯一键去重收集器
|
|
43
|
+
*/
|
|
44
|
+
const unique = new Map<string, LintBaselineEntry>();
|
|
45
|
+
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
/**
|
|
48
|
+
* 条目唯一键
|
|
49
|
+
*/
|
|
50
|
+
const key = `${entry.file}::${entry.rule}`;
|
|
51
|
+
|
|
52
|
+
if (!unique.has(key)) {
|
|
53
|
+
unique.set(key, { ...entry });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [...unique.values()].sort((a, b) => {
|
|
58
|
+
if (a.file !== b.file) {
|
|
59
|
+
return a.file.localeCompare(b.file);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return a.rule.localeCompare(b.rule);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 渲染基线再生后的完整配置文件
|
|
68
|
+
*
|
|
69
|
+
* @param config - 当前解析后配置(其余节值原样保留)
|
|
70
|
+
* @param section - 再生目标节(lint / docs)
|
|
71
|
+
* @param baseline - 再生基线条目(通常为 deriveBaselineEntries 产物)
|
|
72
|
+
* @returns 完整配置文件文本(棘轮契约头 + defineConfig 体)
|
|
73
|
+
* @throws {@link Error} section 为 docs 但配置未登记 docs 节
|
|
74
|
+
*/
|
|
75
|
+
export function renderBaselineRegeneratedConfig(
|
|
76
|
+
config: LziIssuesConfig,
|
|
77
|
+
section: BaselineSection,
|
|
78
|
+
baseline: readonly LintBaselineEntry[],
|
|
79
|
+
): string {
|
|
80
|
+
/**
|
|
81
|
+
* 再生条目(排序去重)
|
|
82
|
+
*/
|
|
83
|
+
const entries = deriveBaselineEntries(baseline);
|
|
84
|
+
|
|
85
|
+
if (section === 'lint') {
|
|
86
|
+
return renderConfigFile(
|
|
87
|
+
{
|
|
88
|
+
schemaVersion: config.schemaVersion,
|
|
89
|
+
storage: config.storage,
|
|
90
|
+
scopes: config.scopes,
|
|
91
|
+
lint: { ...config.lint, baseline: entries },
|
|
92
|
+
numbering: config.numbering,
|
|
93
|
+
slugDigitAllowlist: config.slugDigitAllowlist,
|
|
94
|
+
docs: config.docs,
|
|
95
|
+
close: config.close,
|
|
96
|
+
prompts: config.prompts,
|
|
97
|
+
},
|
|
98
|
+
RATCHET_CONTRACT_HEADER,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (config.docs === undefined) {
|
|
103
|
+
throw new Error('配置未登记 docs 节,无法再生 docs 基线');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return renderConfigFile(
|
|
107
|
+
{
|
|
108
|
+
schemaVersion: config.schemaVersion,
|
|
109
|
+
storage: config.storage,
|
|
110
|
+
scopes: config.scopes,
|
|
111
|
+
lint: config.lint,
|
|
112
|
+
numbering: config.numbering,
|
|
113
|
+
slugDigitAllowlist: config.slugDigitAllowlist,
|
|
114
|
+
docs: { ...config.docs, baseline: entries },
|
|
115
|
+
close: config.close,
|
|
116
|
+
prompts: config.prompts,
|
|
117
|
+
},
|
|
118
|
+
RATCHET_CONTRACT_HEADER,
|
|
119
|
+
);
|
|
120
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置包入口
|
|
3
|
+
*
|
|
4
|
+
* 汇出:defineConfig、schema 与解析、跨条目校验、加载器、init 脚手架渲染、
|
|
5
|
+
* 棘轮基线再生渲染。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from '@/baseline/render-baseline.renderer';
|
|
9
|
+
|
|
10
|
+
export * from '@/loader/load-issues-config.utils';
|
|
11
|
+
|
|
12
|
+
export * from '@/schema/lzi-issues-config.schema';
|
|
13
|
+
|
|
14
|
+
export * from '@/scaffold/render-scaffold.renderer';
|
|
15
|
+
|
|
16
|
+
export * from '@/validation/validate-cross-entries';
|