spec-canon 0.1.0

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/bin/spec-canon.mjs +2 -0
  4. package/dist/commands/init.d.ts +2 -0
  5. package/dist/commands/init.js +97 -0
  6. package/dist/commands/prompt.d.ts +2 -0
  7. package/dist/commands/prompt.js +145 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +11 -0
  10. package/dist/prompts/catalog.d.ts +20 -0
  11. package/dist/prompts/catalog.js +31 -0
  12. package/dist/prompts/renderer.d.ts +5 -0
  13. package/dist/prompts/renderer.js +21 -0
  14. package/dist/templates/index.d.ts +6 -0
  15. package/dist/templates/index.js +32 -0
  16. package/dist/utils/fs.d.ts +5 -0
  17. package/dist/utils/fs.js +29 -0
  18. package/dist/utils/logger.d.ts +8 -0
  19. package/dist/utils/logger.js +26 -0
  20. package/package.json +49 -0
  21. package/templates/00_context.md +48 -0
  22. package/templates/01_requirement.md +40 -0
  23. package/templates/02_interface.md +64 -0
  24. package/templates/03_implementation.md +81 -0
  25. package/templates/04_test_spec.md +34 -0
  26. package/templates/AI_CHANGELOG.md +11 -0
  27. package/templates/claude-md-section.md +66 -0
  28. package/templates/domain_spec.md +38 -0
  29. package/templates/domains-readme.md +21 -0
  30. package/templates/prompts/catalog.json +139 -0
  31. package/templates/prompts/daily/domain_sync.md +48 -0
  32. package/templates/prompts/daily/step0.5_context.md +21 -0
  33. package/templates/prompts/daily/step0.5_context_from_code.md +4 -0
  34. package/templates/prompts/daily/step0.5_context_with_domain.md +11 -0
  35. package/templates/prompts/daily/step0_context.md +21 -0
  36. package/templates/prompts/daily/step0_init.md +2 -0
  37. package/templates/prompts/daily/step1_requirement.md +13 -0
  38. package/templates/prompts/daily/step2_implementation.md +7 -0
  39. package/templates/prompts/daily/step2_interface.md +3 -0
  40. package/templates/prompts/daily/step2_test_spec.md +4 -0
  41. package/templates/prompts/daily/step3_execute.md +3 -0
  42. package/templates/prompts/daily/step4_review.md +4 -0
  43. package/templates/prompts/daily/step5_archive_cross_domain.md +3 -0
  44. package/templates/prompts/daily/step5_archive_domain.md +11 -0
  45. package/templates/prompts/guide.md +86 -0
  46. package/templates/prompts/iterative/cold_batch_baseline.md +15 -0
  47. package/templates/prompts/iterative/cold_batch_identify.md +37 -0
  48. package/templates/prompts/iterative/cold_batch_structure.md +21 -0
  49. package/templates/prompts/iterative/cold_lazy_first_domain_spec.md +9 -0
  50. package/templates/prompts/iterative/split_evaluate.md +16 -0
  51. package/templates/prompts/iterative/split_execute.md +14 -0
  52. package/templates/prompts/new-project/phase0_context_from_prd.md +11 -0
  53. package/templates/prompts/new-project/phase3_first_domain_spec.md +4 -0
  54. package/templates/skill.md +12 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 philo-veritas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # SpecCanon
2
+
3
+ **Spec-Driven Development 框架 —— 让 AI 始终从准确的系统全景出发**
4
+
5
+ > Change specs are temporary; domain specs are canonical.
6
+
7
+ ---
8
+
9
+ ## 什么是 SpecCanon
10
+
11
+ SpecCanon 是一套面向 AI Coding Agent(如 Claude Code)的 **Spec-Driven Development (SDD)** 方法论框架。
12
+
13
+ 核心理念:**先文档后代码,人做判断题,AI 做填空题。**
14
+
15
+ SpecCanon 的本质是一个**文档状态机**:每份 Spec 文档是一个状态节点,状态转移由人触发,AI 负责生成状态内容。通过持续维护 Domain Spec 作为系统行为的单一事实来源(Single Source of Truth),让 AI 在每次开发时都拥有准确的系统全景。
16
+
17
+ ## 核心机制
18
+
19
+ ### 三层文档体系
20
+
21
+ | 层级 | 文件 | 生命周期 | 职责 |
22
+ |---|---|---|---|
23
+ | **系统级** | `domains/README.md` | 持续更新 | 域间关系全景 |
24
+ | **系统级** | `domains/<domain>/domain_spec.md` | 持续累积 | 系统当前行为(Canon) |
25
+ | **Change 级** | `change-xxx/00_context~04_test_spec` | 单次生命周期 | 本次变更做了什么 |
26
+ | **项目级** | `SKILL.md` / `AI_CHANGELOG.md` | 持续累积 | 团队学到了什么 |
27
+
28
+ ### 闭环演化
29
+
30
+ ```
31
+ domain_spec.md(系统正典)
32
+
33
+ │ 新 Change 启动 → 读取系统现状
34
+
35
+ 00_context → 01_requirement → 02_interface → 03_implementation → 04_test_spec
36
+
37
+ │ Change 完成 → 关键信息回填
38
+
39
+ domain_spec.md(演化更新)
40
+ ```
41
+
42
+ 每次 Change 开发都从 Domain Spec 出发,完成后将变更归档回 Domain Spec,形成**知识的持续累积**。Domain Spec 越完善,后续开发的上下文越精准,AI 的产出质量越高。
43
+
44
+ ### 人机分工
45
+
46
+ | 角色 | 职责 | 类比 |
47
+ |---|---|---|
48
+ | 🧑 人(研发工程师) | 决策、审阅、判断对不对 | 做判断题 |
49
+ | 🤖 AI(Coding Agent) | 生成、补全、确保全不全 | 做填空题 |
50
+
51
+ ## 快速开始
52
+
53
+ ### 我要开始一个新项目
54
+ → 阅读 [guide/04_new_project_sop.md](docs/guide/04_new_project_sop.md),然后使用 `spec-canon prompt guide` 查看决策树
55
+
56
+ ### 我要在已有项目中开发新需求
57
+ → 使用 `spec-canon prompt list --stage daily` 查看日常提示词,按 Step 0 → Step 5 顺序执行
58
+
59
+ ### 我要为已有项目引入 SDD
60
+ → 阅读 [guide/05_iterative_project_sop.md](docs/guide/05_iterative_project_sop.md) 的冷启动策略,然后使用 `spec-canon prompt list --stage iterative`
61
+
62
+ ## 文档结构
63
+
64
+ ```
65
+ docs/
66
+ ├── SOP.md ← 导航索引(入口)
67
+ ├── guide/ ← 方法论(一次性阅读)
68
+ │ ├── 01_claude_md.md CLAUDE.md 书写指南
69
+ │ ├── 02_spec_overview.md 三层文档体系总览
70
+ │ ├── 03_spec_documents.md 各文档详解
71
+ │ ├── 04_new_project_sop.md 新项目 SOP
72
+ │ ├── 05_iterative_project_sop.md 迭代项目 SOP
73
+ │ └── 06_domain_splitting.md 子域分拆规则
74
+ ├── prompts/ ← 提示词文件(CLI `spec-canon prompt` 数据源)
75
+ ├── reference/ ← 速查表(随时查阅)
76
+ └── templates/ ← Spec 模板(7 个)
77
+ ```
78
+
79
+ ## 为什么叫 SpecCanon
80
+
81
+ **Canon**(正典)——在 SpecCanon 体系中,`domain_spec.md` 是系统行为的权威来源。每次变更开发完成后,关键信息回填到 Domain Spec,使其成为持续演化的系统正典。
82
+
83
+ - **Spec** 驱动每一次变更
84
+ - **Canon** 保存系统的真相
85
+
86
+ > Specs drive change; Canon preserves the system.
87
+
88
+ ## 许可证
89
+
90
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInitCommand(program: Command): void;
@@ -0,0 +1,97 @@
1
+ import { join, resolve } from 'node:path';
2
+ import { loadTemplate, SPEC_TEMPLATES } from '../templates/index.js';
3
+ import { ensureDir, fileExists, writeFileSafe, } from '../utils/fs.js';
4
+ import { logger } from '../utils/logger.js';
5
+ const SDD_ROOT = 'spec-canon';
6
+ export function registerInitCommand(program) {
7
+ program
8
+ .command('init')
9
+ .description('初始化 SDD 文档结构')
10
+ .option('-d, --dir <path>', '目标项目目录', process.cwd())
11
+ .option('--domain <name>', '同时创建初始业务域')
12
+ .option('--force', '覆盖已存在的 spec-canon 目录', false)
13
+ .action(async (opts) => {
14
+ await runInit(opts);
15
+ });
16
+ }
17
+ async function runInit(opts) {
18
+ const targetDir = resolve(opts.dir);
19
+ const sddDir = join(targetDir, SDD_ROOT);
20
+ // 检查是否已存在
21
+ if (!opts.force && (await fileExists(sddDir))) {
22
+ logger.error(`${SDD_ROOT}/ 目录已存在于 ${targetDir}`);
23
+ logger.info('使用 --force 覆盖已有结构');
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ logger.info(`初始化 SDD 文档结构到 ${targetDir}`);
28
+ logger.blank();
29
+ const created = [];
30
+ // 1. 创建目录树
31
+ const dirs = [
32
+ join(sddDir, 'domains'),
33
+ join(sddDir, 'changes'),
34
+ join(sddDir, 'decisions'),
35
+ join(sddDir, 'skills'),
36
+ join(sddDir, 'templates'),
37
+ ];
38
+ for (const dir of dirs) {
39
+ await ensureDir(dir);
40
+ }
41
+ // 2. 写入 .gitkeep
42
+ await writeFileSafe(join(sddDir, 'changes', '.gitkeep'), '');
43
+ created.push(`${SDD_ROOT}/changes/.gitkeep`);
44
+ // 3. 写入 7 个 Spec 模板
45
+ for (const name of SPEC_TEMPLATES) {
46
+ const content = await loadTemplate(name);
47
+ await writeFileSafe(join(sddDir, 'templates', name), content);
48
+ created.push(`${SDD_ROOT}/templates/${name}`);
49
+ }
50
+ // 4. 写入 AI_CHANGELOG.md
51
+ const changelogContent = await loadTemplate('AI_CHANGELOG.md');
52
+ await writeFileSafe(join(sddDir, 'decisions', 'AI_CHANGELOG.md'), changelogContent);
53
+ created.push(`${SDD_ROOT}/decisions/AI_CHANGELOG.md`);
54
+ // 5. 写入 SKILL.md
55
+ const skillContent = await loadTemplate('skill.md');
56
+ await writeFileSafe(join(sddDir, 'skills', 'SKILL.md'), skillContent);
57
+ created.push(`${SDD_ROOT}/skills/SKILL.md`);
58
+ // 6. 写入 domains/README.md
59
+ const domainsReadme = await loadTemplate('domains-readme.md');
60
+ await writeFileSafe(join(sddDir, 'domains', 'README.md'), domainsReadme);
61
+ created.push(`${SDD_ROOT}/domains/README.md`);
62
+ // 7. 处理 CLAUDE.md
63
+ const claudeMdPath = join(targetDir, 'CLAUDE.md');
64
+ const sddSection = await loadTemplate('claude-md-section.md');
65
+ if (await fileExists(claudeMdPath)) {
66
+ logger.warn('CLAUDE.md 已存在,请手动将以下 SDD 协议段添加到文件中:');
67
+ logger.blank();
68
+ logger.dim('─'.repeat(60));
69
+ console.log(sddSection);
70
+ logger.dim('─'.repeat(60));
71
+ logger.blank();
72
+ }
73
+ else {
74
+ const claudeMdContent = `# 项目:[项目名称]\n[一句话描述项目做什么]\n\n## 技术栈\n- [语言/框架/数据库/中间件]\n\n${sddSection}`;
75
+ await writeFileSafe(claudeMdPath, claudeMdContent);
76
+ created.push('CLAUDE.md');
77
+ logger.success('已创建 CLAUDE.md(含 SDD 协议段)');
78
+ }
79
+ // 8. 处理 --domain
80
+ if (opts.domain) {
81
+ const domainDir = join(sddDir, 'domains', opts.domain);
82
+ await ensureDir(domainDir);
83
+ const domainSpecContent = await loadTemplate('domain_spec.md');
84
+ await writeFileSafe(join(domainDir, 'domain_spec.md'), domainSpecContent);
85
+ created.push(`${SDD_ROOT}/domains/${opts.domain}/domain_spec.md`);
86
+ logger.success(`已创建业务域: ${opts.domain}`);
87
+ }
88
+ // 9. 输出摘要
89
+ logger.blank();
90
+ logger.success(`SDD 初始化完成!共创建 ${created.length} 个文件:`);
91
+ logger.blank();
92
+ for (const file of created) {
93
+ logger.dim(` ${file}`);
94
+ }
95
+ logger.blank();
96
+ logger.info('下一步:编辑 CLAUDE.md 填写项目信息,然后开始你的第一个变更');
97
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPromptCommand(program: Command): void;
@@ -0,0 +1,145 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { findPrompt, getPromptsDir, listPrompts, loadPromptContent, } from '../prompts/catalog.js';
4
+ import { renderPrompt } from '../prompts/renderer.js';
5
+ import { logger } from '../utils/logger.js';
6
+ const STAGE_LABELS = {
7
+ daily: '日常开发 (daily)',
8
+ 'new-project': '新项目 (new-project)',
9
+ iterative: '迭代项目冷启动 (iterative)',
10
+ };
11
+ const STAGE_ORDER = ['daily', 'new-project', 'iterative'];
12
+ export function registerPromptCommand(program) {
13
+ const prompt = program
14
+ .command('prompt')
15
+ .description('输出 SDD 提示词');
16
+ prompt
17
+ .command('list')
18
+ .description('列出所有可用提示词')
19
+ .option('-s, --stage <stage>', '按阶段筛选 (daily / new-project / iterative)')
20
+ .action(async (opts) => {
21
+ await runList(opts.stage);
22
+ });
23
+ prompt
24
+ .command('show <id>')
25
+ .description('输出指定提示词')
26
+ .option('-c, --change <name>', '变更目录名(替换 {{CHANGE}})')
27
+ .option('-d, --domain <name>', '业务域名称(替换 {{DOMAIN}})')
28
+ .option('-m, --module <path>', '目标模块路径(替换 {{MODULE}})')
29
+ .option('-p, --prd <content>', 'PRD 内容/文件引用(替换 {{PRD}})')
30
+ .option('--raw', '输出原始模板(不替换变量)', false)
31
+ .action(async (id, opts) => {
32
+ await runShow(id, opts);
33
+ });
34
+ prompt
35
+ .command('guide')
36
+ .description('显示决策树指南')
37
+ .action(async () => {
38
+ await runGuide();
39
+ });
40
+ }
41
+ async function runList(stage) {
42
+ const prompts = await listPrompts(stage);
43
+ if (prompts.length === 0) {
44
+ if (stage) {
45
+ logger.error(`未找到阶段 "${stage}" 的提示词`);
46
+ logger.info(`可用阶段: ${STAGE_ORDER.join(', ')}`);
47
+ }
48
+ else {
49
+ logger.error('未找到任何提示词');
50
+ }
51
+ process.exitCode = 1;
52
+ return;
53
+ }
54
+ if (stage) {
55
+ // 单阶段输出
56
+ printStageGroup(stage, prompts);
57
+ }
58
+ else {
59
+ // 按阶段分组输出
60
+ const grouped = groupByStage(prompts);
61
+ let first = true;
62
+ for (const s of STAGE_ORDER) {
63
+ const entries = grouped.get(s);
64
+ if (!entries || entries.length === 0)
65
+ continue;
66
+ if (!first)
67
+ console.log();
68
+ printStageGroup(s, entries);
69
+ first = false;
70
+ }
71
+ }
72
+ }
73
+ function printStageGroup(stage, entries) {
74
+ const label = STAGE_LABELS[stage] ?? stage;
75
+ console.log(label);
76
+ for (const entry of entries) {
77
+ const vars = entry.variables.length > 0
78
+ ? ` [${entry.variables.join(', ')}]`
79
+ : '';
80
+ const aliases = entry.aliases && entry.aliases.length > 0
81
+ ? ` (${entry.aliases.join(', ')})`
82
+ : '';
83
+ console.log(` ${entry.id.padEnd(16)} ${entry.name}${aliases}${vars}`);
84
+ }
85
+ }
86
+ function groupByStage(prompts) {
87
+ const map = new Map();
88
+ for (const p of prompts) {
89
+ const list = map.get(p.stage) ?? [];
90
+ list.push(p);
91
+ map.set(p.stage, list);
92
+ }
93
+ return map;
94
+ }
95
+ async function runShow(id, opts) {
96
+ const entry = await findPrompt(id);
97
+ if (!entry) {
98
+ logger.error(`未找到提示词: ${id}`);
99
+ logger.info('使用 "spec-canon prompt list" 查看所有可用提示词');
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const template = await loadPromptContent(entry);
104
+ if (opts.raw) {
105
+ process.stdout.write(template);
106
+ return;
107
+ }
108
+ const variables = {};
109
+ if (opts.change)
110
+ variables['CHANGE'] = opts.change;
111
+ if (opts.domain)
112
+ variables['DOMAIN'] = opts.domain;
113
+ if (opts.module)
114
+ variables['MODULE'] = opts.module;
115
+ if (opts.prd)
116
+ variables['PRD'] = opts.prd;
117
+ const { text, unreplaced } = renderPrompt(template, variables);
118
+ // 提示词 → stdout
119
+ process.stdout.write(text);
120
+ // ctx 提示词缺少 -d/-m 时的专属提示 → stderr
121
+ if (entry.id === 'ctx' && !opts.domain && !opts.module) {
122
+ console.error();
123
+ console.error('⚠ ctx 提示词需要指定 -d(从 domain_spec)或 -m(从代码库)');
124
+ }
125
+ // 未替换变量警告 → stderr
126
+ if (unreplaced.length > 0) {
127
+ const varOpts = {
128
+ CHANGE: '-c, --change',
129
+ DOMAIN: '-d, --domain',
130
+ MODULE: '-m, --module',
131
+ PRD: '-p, --prd',
132
+ };
133
+ console.error();
134
+ console.error("--------------------------------");
135
+ for (const v of unreplaced) {
136
+ const opt = varOpts[v] ?? `--${v.toLowerCase()}`;
137
+ console.error(`⚠ 变量 {{${v}}} 未替换,使用 ${opt} 指定`);
138
+ }
139
+ }
140
+ }
141
+ async function runGuide() {
142
+ const guidePath = join(getPromptsDir(), 'guide.md');
143
+ const content = await readFile(guidePath, 'utf-8');
144
+ process.stdout.write(content);
145
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { Command } from 'commander';
2
+ import { registerInitCommand } from './commands/init.js';
3
+ import { registerPromptCommand } from './commands/prompt.js';
4
+ const program = new Command();
5
+ program
6
+ .name('spec-canon')
7
+ .description('CLI toolkit for Spec-Driven Development (SDD)')
8
+ .version('0.1.0');
9
+ registerInitCommand(program);
10
+ registerPromptCommand(program);
11
+ program.parse();
@@ -0,0 +1,20 @@
1
+ export interface PromptEntry {
2
+ id: string;
3
+ aliases?: string[];
4
+ name: string;
5
+ file: string;
6
+ stage: string;
7
+ step: string;
8
+ description: string;
9
+ variables: string[];
10
+ tags: string[];
11
+ }
12
+ interface CatalogData {
13
+ prompts: PromptEntry[];
14
+ }
15
+ export declare function loadCatalog(): Promise<CatalogData>;
16
+ export declare function findPrompt(id: string): Promise<PromptEntry | undefined>;
17
+ export declare function listPrompts(stage?: string): Promise<PromptEntry[]>;
18
+ export declare function loadPromptContent(entry: PromptEntry): Promise<string>;
19
+ export declare function getPromptsDir(): string;
20
+ export {};
@@ -0,0 +1,31 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const PROMPTS_DIR = resolve(__dirname, '../../templates/prompts');
6
+ let cachedCatalog = null;
7
+ export async function loadCatalog() {
8
+ if (cachedCatalog)
9
+ return cachedCatalog;
10
+ const catalogPath = join(PROMPTS_DIR, 'catalog.json');
11
+ const raw = await readFile(catalogPath, 'utf-8');
12
+ cachedCatalog = JSON.parse(raw);
13
+ return cachedCatalog;
14
+ }
15
+ export async function findPrompt(id) {
16
+ const catalog = await loadCatalog();
17
+ return catalog.prompts.find((p) => p.id === id || p.aliases?.includes(id));
18
+ }
19
+ export async function listPrompts(stage) {
20
+ const catalog = await loadCatalog();
21
+ if (!stage)
22
+ return catalog.prompts;
23
+ return catalog.prompts.filter((p) => p.stage === stage);
24
+ }
25
+ export async function loadPromptContent(entry) {
26
+ const filePath = join(PROMPTS_DIR, entry.file);
27
+ return readFile(filePath, 'utf-8');
28
+ }
29
+ export function getPromptsDir() {
30
+ return PROMPTS_DIR;
31
+ }
@@ -0,0 +1,5 @@
1
+ export interface RenderResult {
2
+ text: string;
3
+ unreplaced: string[];
4
+ }
5
+ export declare function renderPrompt(template: string, variables: Record<string, string>): RenderResult;
@@ -0,0 +1,21 @@
1
+ const IF_BLOCK = /\{\{#if\s+([A-Z_]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
2
+ const VAR_PATTERN = /\{\{([A-Z_]+)\}\}/g;
3
+ export function renderPrompt(template, variables) {
4
+ // 1. 处理条件块:保留已提供变量的块内容,移除未提供的
5
+ const afterBlocks = template
6
+ .replace(IF_BLOCK, (_match, name, body) => {
7
+ return name in variables ? body : '';
8
+ })
9
+ .replace(/\n{3,}/g, '\n\n') // 压缩连续空行
10
+ .trim();
11
+ // 2. 替换剩余的 {{VAR}}
12
+ const unreplaced = [];
13
+ const text = afterBlocks.replace(VAR_PATTERN, (match, name) => {
14
+ if (name in variables) {
15
+ return variables[name];
16
+ }
17
+ unreplaced.push(name);
18
+ return match;
19
+ });
20
+ return { text, unreplaced: [...new Set(unreplaced)] };
21
+ }
@@ -0,0 +1,6 @@
1
+ export declare function loadTemplate(name: string): Promise<string>;
2
+ /** 7 个 Change Spec 模板 */
3
+ export declare const SPEC_TEMPLATES: readonly ["00_context.md", "01_requirement.md", "02_interface.md", "03_implementation.md", "04_test_spec.md", "domain_spec.md", "AI_CHANGELOG.md"];
4
+ /** CLI 独有模板 */
5
+ export declare const CLI_TEMPLATES: readonly ["claude-md-section.md", "domains-readme.md", "skill.md"];
6
+ export declare function loadAllSpecTemplates(): Promise<Map<string, string>>;
@@ -0,0 +1,32 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const TEMPLATES_DIR = resolve(__dirname, '../../templates');
6
+ export async function loadTemplate(name) {
7
+ const filePath = join(TEMPLATES_DIR, name);
8
+ return readFile(filePath, 'utf-8');
9
+ }
10
+ /** 7 个 Change Spec 模板 */
11
+ export const SPEC_TEMPLATES = [
12
+ '00_context.md',
13
+ '01_requirement.md',
14
+ '02_interface.md',
15
+ '03_implementation.md',
16
+ '04_test_spec.md',
17
+ 'domain_spec.md',
18
+ 'AI_CHANGELOG.md',
19
+ ];
20
+ /** CLI 独有模板 */
21
+ export const CLI_TEMPLATES = [
22
+ 'claude-md-section.md',
23
+ 'domains-readme.md',
24
+ 'skill.md',
25
+ ];
26
+ export async function loadAllSpecTemplates() {
27
+ const map = new Map();
28
+ for (const name of SPEC_TEMPLATES) {
29
+ map.set(name, await loadTemplate(name));
30
+ }
31
+ return map;
32
+ }
@@ -0,0 +1,5 @@
1
+ export declare function ensureDir(dirPath: string): Promise<void>;
2
+ export declare function fileExists(filePath: string): Promise<boolean>;
3
+ export declare function writeFileIfNotExists(filePath: string, content: string): Promise<boolean>;
4
+ export declare function writeFileSafe(filePath: string, content: string): Promise<void>;
5
+ export declare function readFileSafe(filePath: string): Promise<string>;
@@ -0,0 +1,29 @@
1
+ import { mkdir, writeFile, access, readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ export async function ensureDir(dirPath) {
4
+ await mkdir(dirPath, { recursive: true });
5
+ }
6
+ export async function fileExists(filePath) {
7
+ try {
8
+ await access(filePath);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ export async function writeFileIfNotExists(filePath, content) {
16
+ if (await fileExists(filePath)) {
17
+ return false;
18
+ }
19
+ await ensureDir(dirname(filePath));
20
+ await writeFile(filePath, content, 'utf-8');
21
+ return true;
22
+ }
23
+ export async function writeFileSafe(filePath, content) {
24
+ await ensureDir(dirname(filePath));
25
+ await writeFile(filePath, content, 'utf-8');
26
+ }
27
+ export async function readFileSafe(filePath) {
28
+ return readFile(filePath, 'utf-8');
29
+ }
@@ -0,0 +1,8 @@
1
+ export declare const logger: {
2
+ success(msg: string): void;
3
+ info(msg: string): void;
4
+ warn(msg: string): void;
5
+ error(msg: string): void;
6
+ dim(msg: string): void;
7
+ blank(): void;
8
+ };
@@ -0,0 +1,26 @@
1
+ const RESET = '\x1b[0m';
2
+ const GREEN = '\x1b[32m';
3
+ const YELLOW = '\x1b[33m';
4
+ const RED = '\x1b[31m';
5
+ const CYAN = '\x1b[36m';
6
+ const DIM = '\x1b[2m';
7
+ export const logger = {
8
+ success(msg) {
9
+ console.log(`${GREEN}✔${RESET} ${msg}`);
10
+ },
11
+ info(msg) {
12
+ console.log(`${CYAN}ℹ${RESET} ${msg}`);
13
+ },
14
+ warn(msg) {
15
+ console.log(`${YELLOW}⚠${RESET} ${msg}`);
16
+ },
17
+ error(msg) {
18
+ console.error(`${RED}✖${RESET} ${msg}`);
19
+ },
20
+ dim(msg) {
21
+ console.log(`${DIM}${msg}${RESET}`);
22
+ },
23
+ blank() {
24
+ console.log();
25
+ },
26
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "spec-canon",
3
+ "version": "0.1.0",
4
+ "description": "CLI toolkit for Spec-Driven Development (SDD)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "philo-veritas",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/philo-veritas/spec-canon.git"
11
+ },
12
+ "homepage": "https://github.com/philo-veritas/spec-canon#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/philo-veritas/spec-canon/issues"
15
+ },
16
+ "keywords": [
17
+ "sdd",
18
+ "spec-driven-development",
19
+ "cli",
20
+ "ai-coding",
21
+ "documentation",
22
+ "spec"
23
+ ],
24
+ "bin": {
25
+ "spec-canon": "./bin/spec-canon.mjs"
26
+ },
27
+ "scripts": {
28
+ "prebuild": "node scripts/sync-templates.mjs",
29
+ "build": "tsc",
30
+ "dev": "tsx src/index.ts",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "files": [
34
+ "dist/",
35
+ "bin/",
36
+ "templates/"
37
+ ],
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "dependencies": {
42
+ "commander": "^13.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^22.0.0",
46
+ "tsx": "^4.0.0",
47
+ "typescript": "^5.7.0"
48
+ }
49
+ }