kld-sdd 2.6.7 → 2.6.8

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.
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const modules = require('./modules.cjs');
6
+ const { readProposalFrontmatter } = require('./naming-diagnose.cjs');
7
+
8
+ function listActiveChanges(projectRoot) {
9
+ const changesDir = path.join(projectRoot, 'openspec', 'changes');
10
+ if (!fs.existsSync(changesDir)) return [];
11
+
12
+ return fs.readdirSync(changesDir)
13
+ .filter((name) => {
14
+ if (name === 'archive' || name.startsWith('.')) return false;
15
+ const full = path.join(changesDir, name);
16
+ return fs.statSync(full).isDirectory();
17
+ })
18
+ .sort();
19
+ }
20
+
21
+ function describeChange(projectRoot, changeName, registry) {
22
+ const changeDir = path.join(projectRoot, 'openspec', 'changes', changeName);
23
+ const proposal = readProposalFrontmatter(changeDir);
24
+ const fm = proposal.ok ? proposal.frontmatter : {};
25
+ const moduleCode = (fm.module || '').toLowerCase();
26
+ const title = fm.title || '';
27
+ const changeKey = (fm['change-key'] || '').toLowerCase() || changeName;
28
+ const affected = Array.isArray(fm['affected-modules']) ? fm['affected-modules'] : [];
29
+ const legacy = !fm.module && !fm.title && !fm['change-key'];
30
+
31
+ let groupCode = 'uncategorized';
32
+ let groupName = '未分类';
33
+ if (!legacy && moduleCode) {
34
+ groupCode = moduleCode;
35
+ groupName = modules.getModuleName(registry, moduleCode) || moduleCode;
36
+ }
37
+
38
+ return {
39
+ changeKey,
40
+ directory: changeName,
41
+ title: title || changeName,
42
+ module: moduleCode || null,
43
+ affectedModules: affected,
44
+ changeId: fm['change-id'] || null,
45
+ legacy,
46
+ groupCode,
47
+ groupName,
48
+ hasProposal: proposal.ok,
49
+ };
50
+ }
51
+
52
+ function groupChanges(projectRoot) {
53
+ const registry = modules.loadModulesYaml(projectRoot);
54
+ const names = listActiveChanges(projectRoot);
55
+ const items = names.map((name) => describeChange(projectRoot, name, registry.ok ? registry : null));
56
+
57
+ const groups = new Map();
58
+ for (const item of items) {
59
+ if (!groups.has(item.groupCode)) {
60
+ groups.set(item.groupCode, {
61
+ code: item.groupCode,
62
+ name: item.groupName,
63
+ changes: [],
64
+ });
65
+ }
66
+ groups.get(item.groupCode).changes.push(item);
67
+ }
68
+
69
+ const ordered = [];
70
+ if (registry.ok) {
71
+ for (const code of Object.keys(registry.modules)) {
72
+ if (groups.has(code)) ordered.push(groups.get(code));
73
+ }
74
+ }
75
+ for (const [code, group] of groups.entries()) {
76
+ if (!ordered.find((item) => item.code === code)) {
77
+ ordered.push(group);
78
+ }
79
+ }
80
+
81
+ return {
82
+ registryOk: registry.ok,
83
+ modulesPath: registry.path || path.join(projectRoot, 'modules.yaml'),
84
+ groups: ordered,
85
+ changes: items,
86
+ };
87
+ }
88
+
89
+ function formatGroupedText(result) {
90
+ const lines = [];
91
+ for (const group of result.groups) {
92
+ lines.push(`${group.name} (${group.code})`);
93
+ for (const change of group.changes) {
94
+ lines.push(` ● ${change.title}`);
95
+ lines.push(` ${change.changeKey}`);
96
+ if (change.module === 'cross' && change.affectedModules.length) {
97
+ lines.push(` affected: ${change.affectedModules.join(', ')}`);
98
+ }
99
+ }
100
+ lines.push('');
101
+ }
102
+ return lines.join('\n').trimEnd();
103
+ }
104
+
105
+ module.exports = {
106
+ listActiveChanges,
107
+ describeChange,
108
+ groupChanges,
109
+ formatGroupedText,
110
+ };
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { MODULE_CODE_REGEX, RESERVED_CROSS } = require('./change-key.cjs');
6
+
7
+ const CODES = {
8
+ MISSING: 'MODULES_YAML_MISSING',
9
+ FORMAT_INVALID: 'MODULES_YAML_FORMAT_INVALID',
10
+ MODULE_INVALID: 'MODULES_CODE_INVALID',
11
+ };
12
+
13
+ /**
14
+ * Minimal parser for modules.yaml constrained schema:
15
+ *
16
+ * version: 1
17
+ * modules:
18
+ * fi:
19
+ * name: 财务核算
20
+ */
21
+ function parseModulesYaml(text) {
22
+ const lines = String(text || '').split(/\r?\n/);
23
+ const modules = {};
24
+ let version = null;
25
+ let inModules = false;
26
+ let currentCode = null;
27
+
28
+ for (let index = 0; index < lines.length; index += 1) {
29
+ const raw = lines[index];
30
+ const line = raw.replace(/#.*$/, '').replace(/\s+$/, '');
31
+ if (!line.trim()) continue;
32
+
33
+ const versionMatch = /^version:\s*(\d+)\s*$/.exec(line);
34
+ if (versionMatch) {
35
+ version = Number(versionMatch[1]);
36
+ inModules = false;
37
+ currentCode = null;
38
+ continue;
39
+ }
40
+
41
+ if (/^modules:\s*$/.test(line)) {
42
+ inModules = true;
43
+ currentCode = null;
44
+ continue;
45
+ }
46
+
47
+ if (!inModules) {
48
+ continue;
49
+ }
50
+
51
+ const moduleMatch = /^ {2}([a-z][a-z0-9]{0,7}):\s*$/.exec(line);
52
+ if (moduleMatch) {
53
+ currentCode = moduleMatch[1];
54
+ modules[currentCode] = { name: '' };
55
+ continue;
56
+ }
57
+
58
+ const nameMatch = /^ {4}name:\s*(.+?)\s*$/.exec(line);
59
+ if (nameMatch && currentCode) {
60
+ let name = nameMatch[1].trim();
61
+ if (
62
+ (name.startsWith('"') && name.endsWith('"')) ||
63
+ (name.startsWith("'") && name.endsWith("'"))
64
+ ) {
65
+ name = name.slice(1, -1);
66
+ }
67
+ modules[currentCode].name = name;
68
+ continue;
69
+ }
70
+
71
+ return {
72
+ ok: false,
73
+ code: CODES.FORMAT_INVALID,
74
+ message: `modules.yaml 第 ${index + 1} 行无法解析: ${raw}`,
75
+ };
76
+ }
77
+
78
+ if (version == null) {
79
+ return {
80
+ ok: false,
81
+ code: CODES.FORMAT_INVALID,
82
+ message: 'modules.yaml 缺少 version',
83
+ };
84
+ }
85
+ if (version !== 1) {
86
+ return {
87
+ ok: false,
88
+ code: CODES.FORMAT_INVALID,
89
+ message: `不支持的 modules.yaml version: ${version}`,
90
+ };
91
+ }
92
+ if (Object.keys(modules).length === 0) {
93
+ return {
94
+ ok: false,
95
+ code: CODES.FORMAT_INVALID,
96
+ message: 'modules.yaml 未声明任何模块',
97
+ };
98
+ }
99
+
100
+ for (const [code, meta] of Object.entries(modules)) {
101
+ if (!MODULE_CODE_REGEX.test(code)) {
102
+ return {
103
+ ok: false,
104
+ code: CODES.MODULE_INVALID,
105
+ message: `模块代号不合规: ${code}`,
106
+ };
107
+ }
108
+ if (!meta.name) {
109
+ return {
110
+ ok: false,
111
+ code: CODES.FORMAT_INVALID,
112
+ message: `模块 ${code} 缺少 name`,
113
+ };
114
+ }
115
+ }
116
+
117
+ if (!modules[RESERVED_CROSS]) {
118
+ modules[RESERVED_CROSS] = { name: '跨模块' };
119
+ }
120
+
121
+ return {
122
+ ok: true,
123
+ version,
124
+ modules,
125
+ codes: new Set(Object.keys(modules)),
126
+ };
127
+ }
128
+
129
+ function loadModulesYaml(projectRoot) {
130
+ const filePath = path.join(projectRoot, 'modules.yaml');
131
+ if (!fs.existsSync(filePath)) {
132
+ return {
133
+ ok: false,
134
+ code: CODES.MISSING,
135
+ message: `modules.yaml 不存在: ${filePath}`,
136
+ path: filePath,
137
+ };
138
+ }
139
+ const text = fs.readFileSync(filePath, 'utf8');
140
+ const parsed = parseModulesYaml(text);
141
+ if (!parsed.ok) {
142
+ return { ...parsed, path: filePath };
143
+ }
144
+ return {
145
+ ...parsed,
146
+ path: filePath,
147
+ };
148
+ }
149
+
150
+ function getModuleName(registry, code) {
151
+ if (!registry || !registry.modules) return null;
152
+ const meta = registry.modules[String(code || '').toLowerCase()];
153
+ return meta ? meta.name : null;
154
+ }
155
+
156
+ function hasModule(registry, code) {
157
+ if (!registry || !registry.codes) return false;
158
+ return registry.codes.has(String(code || '').toLowerCase());
159
+ }
160
+
161
+ module.exports = {
162
+ CODES,
163
+ parseModulesYaml,
164
+ loadModulesYaml,
165
+ getModuleName,
166
+ hasModule,
167
+ };