Skip to content

Commit cb5b8a4

Browse files
committed
进一步优化细节处的中文
1 parent 1f82e1b commit cb5b8a4

File tree

8 files changed

+104
-82
lines changed

8 files changed

+104
-82
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ openspec/
318318
认证和会话管理。
319319

320320
## 需求
321-
### 需求: 用户认证
321+
### 需求 用户认证
322322
系统应在成功登录时签发JWT。
323323

324324
#### 场景: 有效凭据
@@ -332,7 +332,7 @@ openspec/
332332
# 认证增量
333333

334334
## 新增需求
335-
### 需求: 双因素认证
335+
### 需求 双因素认证
336336
系统必须在登录期间要求第二个因素。
337337

338338
#### 场景: 需要OTP
@@ -370,7 +370,7 @@ openspec/
370370
- **`## 移除的需求`** - 弃用的功能
371371

372372
**格式要求:**
373-
- 使用 `### 需求: <名称>` 作为标题
373+
- 使用 `### 需求 <名称>` 作为标题
374374
- 每个需求至少需要一个 `#### 场景:`
375375
- 在需求文本中使用SHALL/MUST
376376

src/core/archive.ts

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@ export class ArchiveCommand {
6464
const validator = new Validator();
6565
let hasValidationErrors = false;
6666

67-
// Validate proposal.md (non-blocking unless strict mode desired in future)
67+
// Validate proposal.md (非严格,仅供参考)
6868
const changeFile = path.join(changeDir, 'proposal.md');
6969
try {
7070
await fs.access(changeFile);
7171
const changeReport = await validator.validateChange(changeFile);
72-
// Proposal validation is informative only (do not block archive)
72+
// Proposal 校验仅提供信息,不阻塞归档
7373
if (!changeReport.valid) {
7474
console.log(chalk.yellow(`\nproposal.md中的提案警告(非阻塞):`));
7575
for (const issue of changeReport.issues) {
@@ -92,10 +92,11 @@ export class ArchiveCommand {
9292
const candidatePath = path.join(changeSpecsDir, c.name, 'spec.md');
9393
await fs.access(candidatePath);
9494
const content = await fs.readFile(candidatePath, 'utf-8');
95-
if (/^##\s+(ADDED|MODIFIED|REMOVED|RENAMED)\s+Requirements/m.test(content)) {
95+
if (/^##\s+(|||)\s*$/m.test(content)) {
9696
hasDeltaSpecs = true;
9797
break;
9898
}
99+
99100
} catch {}
100101
}
101102
}
@@ -359,7 +360,7 @@ export class ArchiveCommand {
359360
const name = normalizeRequirementName(add.name);
360361
if (addedNames.has(name)) {
361362
throw new Error(
362-
`${specName} 验证失败 - ADDED 中的重复需求,标题为 "███ Requirement: ${add.name}"`
363+
`${specName} 验证失败 - 新增需求中存在重复,标题为 "### 需求: ${add.name}"`
363364
);
364365
}
365366
addedNames.add(name);
@@ -369,7 +370,7 @@ export class ArchiveCommand {
369370
const name = normalizeRequirementName(mod.name);
370371
if (modifiedNames.has(name)) {
371372
throw new Error(
372-
`${specName} 验证失败 - MODIFIED 中的重复需求,标题为 "### Requirement: ${mod.name}"`
373+
`${specName} 验证失败 - 修改需求中存在重复,标题为 "### 需求: ${mod.name}"`
373374
);
374375
}
375376
modifiedNames.add(name);
@@ -379,7 +380,7 @@ export class ArchiveCommand {
379380
const name = normalizeRequirementName(rem);
380381
if (removedNamesSet.has(name)) {
381382
throw new Error(
382-
`${specName} 验证失败 - REMOVED 中的重复需求,标题为 "### Requirement: ${rem}"`
383+
`${specName} 验证失败 - 移除需求中存在重复,标题为 "### 需求: ${rem}"`
383384
);
384385
}
385386
removedNamesSet.add(name);
@@ -391,12 +392,12 @@ export class ArchiveCommand {
391392
const toNorm = normalizeRequirementName(to);
392393
if (renamedFromSet.has(fromNorm)) {
393394
throw new Error(
394-
`${specName} 验证失败 - RENAMED 中的重复 FROM,标题为 "### Requirement: ${from}"`
395+
`${specName} 验证失败 - 重命名需求中 FROM 存在重复,标题为 "### 需求: ${from}"`
395396
);
396397
}
397398
if (renamedToSet.has(toNorm)) {
398399
throw new Error(
399-
`${specName} 验证失败 - RENAMED 中的重复 TO,标题为 "### Requirement: ${to}"`
400+
`${specName} 验证失败 - 重命名需求中 TO 存在重复,标题为 "### 需求: ${to}"`
400401
);
401402
}
402403
renamedFromSet.add(fromNorm);
@@ -406,32 +407,32 @@ export class ArchiveCommand {
406407
// Pre-validate cross-section conflicts
407408
const conflicts: Array<{ name: string; a: string; b: string }> = [];
408409
for (const n of modifiedNames) {
409-
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'REMOVED' });
410-
if (addedNames.has(n)) conflicts.push({ name: n, a: 'MODIFIED', b: 'ADDED' });
410+
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: '修改需求', b: '移除需求' });
411+
if (addedNames.has(n)) conflicts.push({ name: n, a: '修改需求', b: '新增需求' });
411412
}
412413
for (const n of addedNames) {
413-
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
414+
if (removedNamesSet.has(n)) conflicts.push({ name: n, a: '新增需求', b: '移除需求' });
414415
}
415416
// Renamed interplay: MODIFIED must reference the NEW header, not FROM
416417
for (const { from, to } of plan.renamed) {
417418
const fromNorm = normalizeRequirementName(from);
418419
const toNorm = normalizeRequirementName(to);
419420
if (modifiedNames.has(fromNorm)) {
420421
throw new Error(
421-
`${specName} 验证失败 - 当存在重命名时,MODIFIED 必须引用新标题 "### Requirement: ${to}"`
422+
`${specName} 验证失败 - 当存在重命名时,MODIFIED 必须引用新标题 "### 需求: ${to}"`
422423
);
423424
}
424425
// Detect ADDED colliding with a RENAMED TO
425426
if (addedNames.has(toNorm)) {
426427
throw new Error(
427-
`${specName} 验证失败 - RENAMED TO 标题与 ADDED 冲突,标题为 "### Requirement: ${to}"`
428+
`${specName} 验证失败 - 重命名需求的 TO 与新增需求冲突,标题为 "### 需求: ${to}"`
428429
);
429430
}
430431
}
431432
if (conflicts.length > 0) {
432433
const c = conflicts[0];
433434
throw new Error(
434-
`${specName} 验证失败 - 需求出现在多个部分中(${c.a}${c.b}),标题为 "### Requirement: ${c.name}"`
435+
`${specName} 验证失败 - 需求出现在多个部分中(${c.a}${c.b}),标题为 "### 需求: ${c.name}"`
435436
);
436437
}
437438
const hasAnyDelta = (plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length) > 0;
@@ -470,17 +471,17 @@ export class ArchiveCommand {
470471
const to = normalizeRequirementName(r.to);
471472
if (!nameToBlock.has(from)) {
472473
throw new Error(
473-
`${specName} RENAMED 失败,标题 "### Requirement: ${r.from}" - 未找到源`
474+
`${specName} 重命名失败,标题 "### 需求: ${r.from}" - 未找到源`
474475
);
475476
}
476477
if (nameToBlock.has(to)) {
477478
throw new Error(
478-
`${specName} RENAMED 失败,标题 "### Requirement: ${r.to}" - 目标已经存在`
479+
`${specName} 重命名失败,标题 "### 需求: ${r.to}" - 目标已经存在`
479480
);
480481
}
481482
const block = nameToBlock.get(from)!;
482483
// Preserve the original header style (Requirement vs 需求) if possible, but default to consistent new header
483-
const newHeader = block.headerLine.includes('需求') ? `### 需求: ${to}` : `### Requirement: ${to}`;
484+
const newHeader = block.headerLine.includes('需求') ? `### 需求 ${to}` : `### 需求: ${to}`;
484485
const rawLines = block.raw.split('\n');
485486
rawLines[0] = newHeader;
486487
const renamedBlock: RequirementBlock = {
@@ -497,7 +498,7 @@ export class ArchiveCommand {
497498
const key = normalizeRequirementName(name);
498499
if (!nameToBlock.has(key)) {
499500
throw new Error(
500-
`${specName} REMOVED 失败,标题 "### Requirement: ${name}" - 未找到`
501+
`${specName} 移除失败,标题 "### 需求: ${name}" - 未找到`
501502
);
502503
}
503504
nameToBlock.delete(key);
@@ -508,26 +509,28 @@ export class ArchiveCommand {
508509
const key = normalizeRequirementName(mod.name);
509510
if (!nameToBlock.has(key)) {
510511
throw new Error(
511-
`${specName} MODIFIED 失败,标题 "### Requirement: ${mod.name}" - 未找到`
512+
`${specName} 修改失败,标题 "### 需求: ${mod.name}" - 未找到`
512513
);
513514
}
514515
// Replace block with provided raw (ensure header line matches key)
515516
const REQUIREMENT_KEYWORD_PATTERN = '(?:Requirement|需求)';
516-
const modHeaderMatch = mod.raw.split('\n')[0].match(new RegExp(`^###\\s*${REQUIREMENT_KEYWORD_PATTERN}:\\s*(.+)\\s*$`));
517+
const REQUIREMENT_COLON_PATTERN = '[::]';
518+
const modHeaderMatch = mod.raw.split('\n')[0].match(new RegExp(`^###\\s*${REQUIREMENT_KEYWORD_PATTERN}${REQUIREMENT_COLON_PATTERN}\\s*(.+)\\s*$`));
517519
if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
518520
throw new Error(
519-
`${specName} MODIFIED 失败,标题 "### Requirement: ${mod.name}" - 内容中的标题不匹配`
521+
`${specName} 修改失败,标题 "### 需求: ${mod.name}" - 内容中的标题不匹配`
520522
);
521523
}
522524
nameToBlock.set(key, mod);
525+
523526
}
524527

525528
// ADDED
526529
for (const add of plan.added) {
527530
const key = normalizeRequirementName(add.name);
528531
if (nameToBlock.has(key)) {
529532
throw new Error(
530-
`${specName} ADDED failed for header "### Requirement: ${add.name}" - already exists`
533+
`${specName} 新增失败,标题 "### 需求: ${add.name}" - 已经存在`
531534
);
532535
}
533536
nameToBlock.set(key, add);
@@ -598,7 +601,7 @@ export class ArchiveCommand {
598601

599602
private buildSpecSkeleton(specFolderName: string, changeName: string): string {
600603
const titleBase = specFolderName;
601-
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## Requirements\n`;
604+
return `# ${titleBase} Specification\n\n## Purpose\nTBD - created by archiving change ${changeName}. Update Purpose after archive.\n\n## 需求\n`;
602605
}
603606

604607
private getArchiveDate(): string {

src/core/parsers/requirement-blocks.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export interface RequirementBlock {
2-
headerLine: string; // e.g., '### Requirement: Something' or '### 需求: Something'
2+
headerLine: string; // e.g., '### Requirement: Something' or '### 需求 Something'
33
name: string; // e.g., 'Something'
44
raw: string; // full block including headerLine and following content
55
}
@@ -17,13 +17,15 @@ export function normalizeRequirementName(name: string): string {
1717
}
1818

1919
const REQUIREMENT_KEYWORD_PATTERN = '(?:Requirement|需求)';
20-
const REQUIREMENT_HEADER_REGEX = new RegExp(`^###\\s*${REQUIREMENT_KEYWORD_PATTERN}:\\s*(.+)\\s*$`);
21-
const REQUIREMENT_HEADER_PREFIX = new RegExp(`^###\\s+${REQUIREMENT_KEYWORD_PATTERN}:`);
20+
const REQUIREMENT_COLON_PATTERN = '[::]';
21+
const REQUIREMENT_HEADER_REGEX = new RegExp(`^###\\s*${REQUIREMENT_KEYWORD_PATTERN}${REQUIREMENT_COLON_PATTERN}\\s*(.+)\\s*$`);
22+
const REQUIREMENT_HEADER_PREFIX = new RegExp(`^###\\s+${REQUIREMENT_KEYWORD_PATTERN}${REQUIREMENT_COLON_PATTERN}`);
2223

2324
/**
2425
* Extracts the Requirements section from a spec file and parses requirement blocks.
2526
*/
2627
export function extractRequirementsSection(content: string): RequirementsSectionParts {
28+
2729
const normalized = normalizeLineEndings(content);
2830
const lines = normalized.split('\n');
2931
const reqHeaderIndex = lines.findIndex(l => /^##\s+\s*$/i.test(l));
@@ -218,10 +220,12 @@ function parseRemovedNames(sectionBody: string): string[] {
218220
continue;
219221
}
220222
// Also support bullet list of headers
221-
const bullet = line.match(new RegExp(`^\\s*-\\s*\`?###\\s*${REQUIREMENT_KEYWORD_PATTERN}:\\s*(.+?)\`?\\s*$`));
223+
const bullet = line.match(new RegExp(`^\\s*-\\s*` +
224+
'`?###\\s*' + REQUIREMENT_KEYWORD_PATTERN + REQUIREMENT_COLON_PATTERN + '\\s*(.+?)`?\\s*$'));
222225
if (bullet) {
223226
names.push(normalizeRequirementName(bullet[1]));
224227
}
228+
225229
}
226230
return names;
227231
}
@@ -231,9 +235,23 @@ function parseRenamedPairs(sectionBody: string): Array<{ from: string; to: strin
231235
const pairs: Array<{ from: string; to: string }> = [];
232236
const lines = normalizeLineEndings(sectionBody).split('\n');
233237
let current: { from?: string; to?: string } = {};
238+
239+
const fromRegex = new RegExp(
240+
'^\\s*-?\\s*FROM:\\s*`?###\\s*' +
241+
REQUIREMENT_KEYWORD_PATTERN +
242+
REQUIREMENT_COLON_PATTERN +
243+
'\\s*(.+?)`?\\s*$'
244+
);
245+
const toRegex = new RegExp(
246+
'^\\s*-?\\s*TO:\\s*`?###\\s*' +
247+
REQUIREMENT_KEYWORD_PATTERN +
248+
REQUIREMENT_COLON_PATTERN +
249+
'\\s*(.+?)`?\\s*$'
250+
);
251+
234252
for (const line of lines) {
235-
const fromMatch = line.match(new RegExp(`^\\s*-?\\s*FROM:\\s*\`?###\\s*${REQUIREMENT_KEYWORD_PATTERN}:\\s*(.+?)\`?\\s*$`));
236-
const toMatch = line.match(new RegExp(`^\\s*-?\\s*TO:\\s*\`?###\\s*${REQUIREMENT_KEYWORD_PATTERN}:\\s*(.+?)\`?\\s*$`));
253+
const fromMatch = line.match(fromRegex);
254+
const toMatch = !fromMatch && line.match(toRegex);
237255
if (fromMatch) {
238256
current.from = normalizeRequirementName(fromMatch[1]);
239257
} else if (toMatch) {
@@ -244,5 +262,6 @@ function parseRenamedPairs(sectionBody: string): Array<{ from: string; to: strin
244262
}
245263
}
246264
}
265+
247266
return pairs;
248267
}

src/core/templates/agents-template.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ AI编程助手使用OpenSpec进行规范驱动开发的说明文档。
8484
- 显示详情:
8585
- 规范:\`openspec-cn show <spec-id> --type spec\`(使用\`--json\`进行过滤)
8686
- 变更:\`openspec-cn show <change-id> --json --deltas-only\`
87-
- 全文搜索(使用ripgrep):\`rg -n "Requirement:|Scenario:|需求:|场景:" openspec/specs\`
87+
- 全文搜索(使用ripgrep):\`rg -n "Requirement:|Scenario:|需求|场景" openspec/specs\`
8888
8989
## 快速开始
9090
@@ -322,8 +322,8 @@ openspec-cn show [spec] --json -r 1
322322
openspec-cn spec list --long
323323
openspec-cn list
324324
# 可选全文搜索:
325-
# rg -n "Requirement:|Scenario:|需求:|场景:" openspec/specs
326-
# rg -n "^#|Requirement:|需求:" openspec/changes
325+
# rg -n "Requirement:|Scenario:|需求|场景" openspec/specs
326+
# rg -n "^#|Requirement:|需求" openspec/changes
327327
328328
# 2) 选择变更id并创建骨架
329329
CHANGE=add-two-factor-auth

src/core/templates/slash-command-templates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const proposalSteps = `**步骤**
2020

2121
const proposalReferences = `**参考**
2222
- 当验证失败时,使用\`openspec-cn show <id> --json --deltas-only\`或\`openspec-cn show <spec> --type spec\`检查详情。
23-
- 在编写新需求前,使用\`rg -n "需求:|场景:" openspec/specs\`搜索现有需求。
23+
- 在编写新需求前,使用\`rg -n "需求|场景" openspec/specs\`搜索现有需求。
2424
- 使用\`rg <keyword>\`、\`ls\`或直接文件读取探索代码库,以便提案与当前实现现实保持一致。`;
2525

2626
const applySteps = `**步骤**

src/core/validation/validator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export class Validator {
253253
issues.push({
254254
level: 'ERROR',
255255
path: specPath,
256-
message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求:" 块(移除需求部分可以使用项目符号列表语法)。`,
256+
message: `找到了增量部分 ${this.formatSectionList(sections)},但未解析到任何需求条目。请确保每个部分至少包含一个 "### 需求" 块(移除需求部分可以使用项目符号列表语法)。`,
257257
});
258258
}
259259
for (const path of missingHeaderSpecs) {
@@ -444,6 +444,6 @@ export class Validator {
444444
if (sections.length === 1) return sections[0];
445445
const head = sections.slice(0, -1);
446446
const last = sections[sections.length - 1];
447-
return `${head.join(', ')} and ${last}`;
447+
return `${head.join('')} ${last}`;
448448
}
449-
}
449+
}

0 commit comments

Comments
 (0)