@chenguangyao/devflow-kit 0.1.43

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 (198) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/bin/devflow.js +9 -0
  5. package/docs/RFC-001-devflow-kit.md +617 -0
  6. package/docs/RFC-002-workflow-kernel.md +134 -0
  7. package/docs/enterprise-integration-supplement.md +274 -0
  8. package/docs/internal-gitlab-setup.md +426 -0
  9. package/docs/marketplace-skills.md +231 -0
  10. package/docs/migration-from-arb.md +232 -0
  11. package/docs/tooling-overview.md +774 -0
  12. package/docs/workflow-orchestration.md +695 -0
  13. package/docs/workflow-ui-prototype.html +271 -0
  14. package/package.json +52 -0
  15. package/schemas/config.schema.json +51 -0
  16. package/schemas/delta.schema.json +22 -0
  17. package/schemas/state.schema.json +130 -0
  18. package/schemas/status-surface.schema.json +197 -0
  19. package/schemas/workflow-confirmation-surface.schema.json +70 -0
  20. package/schemas/workflow-picker.schema.json +94 -0
  21. package/scripts/postinstall.js +101 -0
  22. package/scripts/render-workflow-ui-prototype.js +271 -0
  23. package/skills/apply/SKILL.md +313 -0
  24. package/skills/apply/references/discipline-checklist.md +145 -0
  25. package/skills/apply/references/subagent-implementer-prompt.md +113 -0
  26. package/skills/apply/references/subagent-orchestration.md +150 -0
  27. package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
  28. package/skills/apply/references/tdd-loop.md +287 -0
  29. package/skills/apply/references/when-plan-is-wrong.md +279 -0
  30. package/skills/apply/references/worktree-swarm.md +292 -0
  31. package/skills/archive/SKILL.md +229 -0
  32. package/skills/archive/references/conflict-resolution.md +336 -0
  33. package/skills/archive/references/knowledge-deposit.md +381 -0
  34. package/skills/archive/references/spec-merge.md +365 -0
  35. package/skills/brainstorm/SKILL.md +123 -0
  36. package/skills/brainstorm/references/proposal-template.md +244 -0
  37. package/skills/brainstorm/references/question-catalog.md +168 -0
  38. package/skills/brainstorm/references/session-template.md +184 -0
  39. package/skills/ci-fix/SKILL.md +63 -0
  40. package/skills/ci-fix/references/loop.md +25 -0
  41. package/skills/code-review/SKILL.md +279 -0
  42. package/skills/code-review/references/escalation-playbook.md +192 -0
  43. package/skills/code-review/references/language-cheatsheets/go.md +175 -0
  44. package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
  45. package/skills/code-review/references/language-cheatsheets/python.md +170 -0
  46. package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
  47. package/skills/code-review/references/output-template.md +275 -0
  48. package/skills/code-review/references/review-checklist.md +251 -0
  49. package/skills/complexity-grading/SKILL.md +259 -0
  50. package/skills/deliver/SKILL.md +271 -0
  51. package/skills/deliver/references/delivery-modes.md +299 -0
  52. package/skills/deliver/references/notify.md +359 -0
  53. package/skills/deliver/references/pr-description.md +319 -0
  54. package/skills/dependency-upgrade/SKILL.md +57 -0
  55. package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
  56. package/skills/df-orchestrator/SKILL.md +407 -0
  57. package/skills/df-orchestrator/references/complexity-grading.md +177 -0
  58. package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
  59. package/skills/df-orchestrator/references/routing-rules.md +290 -0
  60. package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
  61. package/skills/frontend-quality/SKILL.md +61 -0
  62. package/skills/frontend-quality/references/checklist.md +35 -0
  63. package/skills/handoff-resume/SKILL.md +59 -0
  64. package/skills/handoff-resume/references/handoff-template.md +54 -0
  65. package/skills/plan/SKILL.md +166 -0
  66. package/skills/plan/references/task-breakdown.md +207 -0
  67. package/skills/plan/references/task-sequencing.md +143 -0
  68. package/skills/plan/references/task-template.md +248 -0
  69. package/skills/requirement-analysis/SKILL.md +499 -0
  70. package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
  71. package/skills/requirement-analysis/references/code-recon.md +151 -0
  72. package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
  73. package/skills/requirement-analysis/references/requirement-template.md +339 -0
  74. package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
  75. package/skills/security-hardening/SKILL.md +60 -0
  76. package/skills/security-hardening/references/checklist.md +42 -0
  77. package/skills/tech-spec/SKILL.md +388 -0
  78. package/skills/tech-spec/references/api-contract-design.md +172 -0
  79. package/skills/tech-spec/references/decision-records.md +110 -0
  80. package/skills/tech-spec/references/design-template.md +301 -0
  81. package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
  82. package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
  83. package/skills/tech-spec/references/transaction-patterns.md +212 -0
  84. package/skills/test-spec/SKILL.md +219 -0
  85. package/skills/test-spec/references/coverage-strategy.md +218 -0
  86. package/skills/test-spec/references/edge-case-to-test.md +143 -0
  87. package/skills/test-spec/references/test-case-template.md +276 -0
  88. package/skills/verify/SKILL.md +232 -0
  89. package/skills/verify/references/nfr-verification.md +292 -0
  90. package/skills/verify/references/report-templates.md +510 -0
  91. package/skills/verify/references/self-test-guide.md +240 -0
  92. package/skills/verify/references/verify-rollback-map.md +247 -0
  93. package/src/cli/commands/_helpers.js +108 -0
  94. package/src/cli/commands/_submit.js +718 -0
  95. package/src/cli/commands/apply.js +198 -0
  96. package/src/cli/commands/archive.js +180 -0
  97. package/src/cli/commands/checkpoint.js +113 -0
  98. package/src/cli/commands/deliver.js +377 -0
  99. package/src/cli/commands/deploy.js +504 -0
  100. package/src/cli/commands/design.js +158 -0
  101. package/src/cli/commands/disable.js +21 -0
  102. package/src/cli/commands/doctor.js +178 -0
  103. package/src/cli/commands/enable.js +21 -0
  104. package/src/cli/commands/flow.js +645 -0
  105. package/src/cli/commands/help.js +93 -0
  106. package/src/cli/commands/ingest.js +602 -0
  107. package/src/cli/commands/init.js +341 -0
  108. package/src/cli/commands/knowledge.js +523 -0
  109. package/src/cli/commands/logs.js +43 -0
  110. package/src/cli/commands/new.js +202 -0
  111. package/src/cli/commands/plan.js +49 -0
  112. package/src/cli/commands/propose.js +27 -0
  113. package/src/cli/commands/provider.js +698 -0
  114. package/src/cli/commands/report.js +143 -0
  115. package/src/cli/commands/requirement.js +227 -0
  116. package/src/cli/commands/review.js +301 -0
  117. package/src/cli/commands/skills.js +457 -0
  118. package/src/cli/commands/status.js +925 -0
  119. package/src/cli/commands/switch.js +27 -0
  120. package/src/cli/commands/sync.js +47 -0
  121. package/src/cli/commands/test.js +366 -0
  122. package/src/cli/commands/uninstall.js +32 -0
  123. package/src/cli/commands/update.js +74 -0
  124. package/src/cli/commands/verify.js +354 -0
  125. package/src/cli/commands/worktree.js +78 -0
  126. package/src/cli/index.js +72 -0
  127. package/src/cli/parse-args.js +102 -0
  128. package/src/core/autodetect.js +271 -0
  129. package/src/core/change.js +208 -0
  130. package/src/core/checkpoint.js +217 -0
  131. package/src/core/config.js +60 -0
  132. package/src/core/delta.js +290 -0
  133. package/src/core/markers.js +59 -0
  134. package/src/core/paths.js +173 -0
  135. package/src/core/plan-tasks.js +36 -0
  136. package/src/core/project-routing.js +285 -0
  137. package/src/core/projects.js +200 -0
  138. package/src/core/state.js +200 -0
  139. package/src/core/workflow-check.js +177 -0
  140. package/src/core/workflow-init.js +34 -0
  141. package/src/core/workflow-picker.js +154 -0
  142. package/src/core/workflow-policy.js +119 -0
  143. package/src/core/workflow-suggest.js +181 -0
  144. package/src/core/workflow-verify.js +88 -0
  145. package/src/core/workflow.js +433 -0
  146. package/src/core/worktree.js +241 -0
  147. package/src/knowledge/categories.js +107 -0
  148. package/src/knowledge/classify.js +125 -0
  149. package/src/knowledge/deposit.js +414 -0
  150. package/src/knowledge/migrate.js +149 -0
  151. package/src/knowledge/mr.js +219 -0
  152. package/src/knowledge/query.js +131 -0
  153. package/src/knowledge/registry.js +151 -0
  154. package/src/knowledge/sync.js +179 -0
  155. package/src/providers/base.js +74 -0
  156. package/src/providers/drivers/api-yapi.js +78 -0
  157. package/src/providers/drivers/ci-jenkins.js +109 -0
  158. package/src/providers/drivers/intake-confluence.js +544 -0
  159. package/src/providers/drivers/kb-git.js +549 -0
  160. package/src/providers/drivers/kb-weknora.js +472 -0
  161. package/src/providers/drivers/notify-smtp.js +515 -0
  162. package/src/providers/drivers/observability-oss.js +43 -0
  163. package/src/providers/drivers/observability-sls.js +50 -0
  164. package/src/providers/lifecycle.js +135 -0
  165. package/src/providers/loader.js +132 -0
  166. package/src/providers/local.js +190 -0
  167. package/src/providers/userconfig.js +283 -0
  168. package/src/reports/aggregate.js +185 -0
  169. package/src/reports/coverage.js +163 -0
  170. package/src/reports/detect.js +143 -0
  171. package/src/reports/parse.js +236 -0
  172. package/src/templates/files/ci/github.yml +38 -0
  173. package/src/templates/files/ci/gitlab.yml +27 -0
  174. package/src/templates/files/design.md +63 -0
  175. package/src/templates/files/ide/devflow-workflow.md +58 -0
  176. package/src/templates/files/ide/project-overview-reference.md +1 -0
  177. package/src/templates/files/ide/project-overview.md +27 -0
  178. package/src/templates/files/knowledge-index.json +17 -0
  179. package/src/templates/files/knowledge.md +28 -0
  180. package/src/templates/files/meta.json +8 -0
  181. package/src/templates/files/plan.md +38 -0
  182. package/src/templates/files/proposal.md +33 -0
  183. package/src/templates/files/reports/contract-test.md +40 -0
  184. package/src/templates/files/reports/e2e-test.md +30 -0
  185. package/src/templates/files/reports/integration-test.md +36 -0
  186. package/src/templates/files/reports/joint-test.md +58 -0
  187. package/src/templates/files/reports/perf.md +24 -0
  188. package/src/templates/files/reports/regression.md +20 -0
  189. package/src/templates/files/reports/remote-test.md +55 -0
  190. package/src/templates/files/reports/self-test.md +43 -0
  191. package/src/templates/files/reports/smoke-test.md +22 -0
  192. package/src/templates/files/reports/unit-test.md +36 -0
  193. package/src/templates/files/requirement.md +51 -0
  194. package/src/templates/files/review.md +38 -0
  195. package/src/templates/files/tests.md +36 -0
  196. package/src/templates/files/verify.md +32 -0
  197. package/src/templates/index.js +21 -0
  198. package/src/utils/log.js +37 -0
@@ -0,0 +1,414 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../core/paths.js');
6
+ const { ensureMeta, writeMeta, sha256 } = require('./registry.js');
7
+ const { categoryDir } = require('./categories.js');
8
+
9
+ /**
10
+ * Knowledge deposit strategy: **artifact-first, not section-first**.
11
+ *
12
+ * Each source file (knowledge/*.md, design.md, incident-*.md) produces AT MOST
13
+ * ONE knowledge document — the whole file is deposited as a single document.
14
+ *
15
+ * This mirrors the arb-workflow-kit pattern where one change yields 2-4 files
16
+ * rather than 10+ fragmented section files.
17
+ *
18
+ * Exceptions (both produce ADDITIONAL files, not replacements):
19
+ * <!-- knowledge:skip --> — exclude entire source file from deposit
20
+ * <!-- knowledge:domain=X category=Y filename=foo.md -->
21
+ * — specific section gets its own targeted file
22
+ *
23
+ * Source → Category mapping:
24
+ * design.md → decisions "[slug] <title> - 技术方案.md"
25
+ * knowledge/*.md → category by dir keep filename
26
+ * incident-*.md → incidents "[slug] <name> - 故障复盘.md"
27
+ * regression*.md → runbooks "[slug] regression - 回归测试.md"
28
+ *
29
+ * Returns a plan: [{ source, title, target, targetAbs, action, sha256, domain, category, filename, body }]
30
+ */
31
+
32
+ const ARTIFACT_MAP = [
33
+ { match: /(^|\/)design\.md$/i, category: 'decisions' },
34
+ { match: /(^|\/)requirement\.md$/i, category: 'scenarios' },
35
+ { match: /(^|\/)proposal\.md$/i, category: 'scenarios' },
36
+ { match: /(^|\/)incident[-_]?.*\.md$/i, category: 'incidents' },
37
+ { match: /(^|\/)regression.*\.md$/i, category: 'runbooks' },
38
+ { match: /(^|\/)adr[-_]?\d*\.md$/i, category: 'decisions' },
39
+ ];
40
+
41
+ const KIND_MAP = {
42
+ concepts: 'concept',
43
+ rules: 'rule',
44
+ scenarios: 'scenario',
45
+ contracts: 'contract',
46
+ decisions: 'adr',
47
+ services: 'service',
48
+ runbooks: 'runbook',
49
+ incidents: 'incident',
50
+ environments: 'environment',
51
+ solutions: 'solution-pattern',
52
+ };
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+
56
+ /**
57
+ * Find the change directory for a slug (active or archived).
58
+ */
59
+ function resolveChangeDir(root, slug) {
60
+ const workspace = paths.workspaceChangesDir(slug);
61
+ if (fsSync.existsSync(workspace)) return workspace;
62
+ const live = path.join(paths.devflowDir(root), 'changes', slug);
63
+ if (fsSync.existsSync(live)) return live;
64
+ const archDir = paths.archiveDir(root);
65
+ if (!fsSync.existsSync(archDir)) return null;
66
+ const entries = require('fs').readdirSync(archDir, { withFileTypes: true });
67
+ for (const e of entries) {
68
+ if (!e.isDirectory()) continue;
69
+ if (e.name === slug || e.name.endsWith('-' + slug)) {
70
+ return path.join(archDir, e.name);
71
+ }
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Build the deposit plan (dry-run safe).
78
+ * Returns [{ source, title, target, targetAbs, action, sha256, domain, category, filename, body }]
79
+ */
80
+ async function deposit(root, opts = {}) {
81
+ const { slug, dryRun = false, force = false } = opts;
82
+ if (!slug) throw new Error('deposit requires slug');
83
+
84
+ const cdir = resolveChangeDir(root, slug);
85
+ if (!cdir) throw new Error(`change not found: ${slug}`);
86
+
87
+ const sources = await collectSources(cdir);
88
+ const plan = [];
89
+
90
+ for (const src of sources) {
91
+ const rawFile = await fs.readFile(src.abs, 'utf8');
92
+ const raw = /^knowledge[\\/]/.test(src.rel)
93
+ ? stripDepositFrontmatter(rawFile)
94
+ : rawFile;
95
+
96
+ // 1. Whole-file skip directive
97
+ if (/<!--\s*knowledge:\s*skip\b/i.test(raw)) continue;
98
+
99
+ // 2. Handle explicit per-section overrides FIRST (<!-- knowledge:domain=X category=Y -->)
100
+ const explicit = extractExplicitSections(raw);
101
+ for (const sec of explicit) {
102
+ const entry = await buildEntry(root, sec.body, sec.category, sec.filename || null, src.rel, slug, { force });
103
+ if (entry) plan.push(entry);
104
+ }
105
+
106
+ // 3. Deposit the remaining content as ONE document
107
+ const cleaned = removeExplicitSections(raw);
108
+ if (!cleaned.trim()) continue;
109
+
110
+ const target = artifactCategory(src.rel);
111
+ if (!target) continue; // unknown artifact type; skip
112
+
113
+ const title = extractTitle(raw, src.rel, slug);
114
+ const fname = buildFilename(slug, title, src.rel);
115
+ const entry = await buildEntry(root, cleaned, target.category, fname, src.rel, slug, { force });
116
+ if (entry) plan.push(entry);
117
+ }
118
+
119
+ if (!dryRun) {
120
+ await executeItems(root, plan.filter((p) => p.action !== 'noop'), slug);
121
+ }
122
+ return plan;
123
+ }
124
+
125
+ function stripDepositFrontmatter(text) {
126
+ const m = /^---\s*\n([\s\S]*?)\n---\s*\n?/m.exec(text);
127
+ if (!m) return text;
128
+ if (!/^deposited_at:/m.test(m[1]) && !/^sha256:/m.test(m[1])) return text;
129
+ return text.slice(m[0].length);
130
+ }
131
+
132
+ // ─────────────────────────────────────────────────────────────────────────────
133
+ // Helpers
134
+ // ─────────────────────────────────────────────────────────────────────────────
135
+
136
+ async function collectSources(cdir) {
137
+ const out = [];
138
+ for (const f of ['design.md']) {
139
+ const abs = path.join(cdir, f);
140
+ if (fsSync.existsSync(abs)) out.push({ rel: f, abs });
141
+ }
142
+ const knowledgeDir = path.join(cdir, 'knowledge');
143
+ if (fsSync.existsSync(knowledgeDir)) {
144
+ await collectKnowledgeSources(knowledgeDir, knowledgeDir, out);
145
+ }
146
+ const reportsDir = path.join(cdir, 'reports');
147
+ if (fsSync.existsSync(reportsDir)) {
148
+ for (const f of await fs.readdir(reportsDir)) {
149
+ if (!f.endsWith('.md')) continue;
150
+ if (/^(incident|regression)/i.test(f)) {
151
+ out.push({ rel: 'reports/' + f, abs: path.join(reportsDir, f) });
152
+ }
153
+ }
154
+ }
155
+ return out;
156
+ }
157
+
158
+ async function collectKnowledgeSources(baseDir, dir, out) {
159
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
160
+ const abs = path.join(dir, entry.name);
161
+ if (entry.isDirectory()) {
162
+ await collectKnowledgeSources(baseDir, abs, out);
163
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
164
+ out.push({ rel: path.join('knowledge', path.relative(baseDir, abs)), abs });
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Extract sections with explicit knowledge: hints into standalone items.
171
+ * Each returns { body, domain, category, filename? }.
172
+ */
173
+ function extractExplicitSections(text) {
174
+ const sections = [];
175
+ const lines = text.split('\n');
176
+ let pending = null; // { domain, category, filename, lines[] }
177
+
178
+ for (const line of lines) {
179
+ const hint = parseKnowledgeHint(line.trim());
180
+ if (hint && hint.domain && hint.category) {
181
+ // Flush previous pending
182
+ if (pending) sections.push({ ...pending, body: pending.lines.join('\n') });
183
+ pending = { domain: hint.domain, category: hint.category, filename: hint.filename || null, lines: [] };
184
+ continue;
185
+ }
186
+ if (pending) {
187
+ // Close on next H2 that doesn't have another hint right above it
188
+ if (/^## /.test(line) && !hint) {
189
+ sections.push({ ...pending, body: pending.lines.join('\n') });
190
+ pending = null;
191
+ } else {
192
+ pending.lines.push(line);
193
+ }
194
+ }
195
+ }
196
+ if (pending) sections.push({ ...pending, body: pending.lines.join('\n') });
197
+ return sections.filter((s) => s.body.trim());
198
+ }
199
+
200
+ /**
201
+ * Strip explicit sections (<!-- knowledge:domain=... --> blocks) from text
202
+ * so they aren't double-deposited in the whole-file entry.
203
+ */
204
+ function removeExplicitSections(text) {
205
+ const lines = text.split('\n');
206
+ const out = [];
207
+ let inExplicit = false;
208
+
209
+ for (const line of lines) {
210
+ const hint = parseKnowledgeHint(line.trim());
211
+ if (hint && hint.domain && hint.category) {
212
+ inExplicit = true;
213
+ continue;
214
+ }
215
+ if (inExplicit && /^## /.test(line)) {
216
+ inExplicit = false;
217
+ }
218
+ if (!inExplicit) out.push(line);
219
+ }
220
+ return out.join('\n');
221
+ }
222
+
223
+ function parseKnowledgeHint(str) {
224
+ const m = /^<!--\s*knowledge:([^>]*?)-->/i.exec(str);
225
+ if (!m) return null;
226
+ const hint = {};
227
+ for (const part of m[1].trim().split(/\s+/)) {
228
+ const kv = /^(\w+)=(.+)$/.exec(part);
229
+ if (kv) hint[kv[1]] = kv[2];
230
+ }
231
+ if (m[1].trim() === 'skip') hint.skip = true;
232
+ return hint;
233
+ }
234
+
235
+ function artifactCategory(rel) {
236
+ const parts = rel.split(/[\\/]/);
237
+ if (parts[0] === 'knowledge' && parts.length >= 3) {
238
+ const { CATEGORY_TAG_NAMES } = require('./categories.js');
239
+ const category = Object.entries(CATEGORY_TAG_NAMES)
240
+ .find(([, info]) => info.name === parts[1])?.[0] || parts[1];
241
+ return { category };
242
+ }
243
+ for (const r of ARTIFACT_MAP) {
244
+ if (r.match.test(rel)) return { category: r.category };
245
+ }
246
+ return null;
247
+ }
248
+
249
+ /**
250
+ * Extract a meaningful title from the document.
251
+ * Priority: frontmatter title: → first # heading → fallback to artifact name
252
+ *
253
+ * Strips common auto-generated prefixes that appear in headings:
254
+ * "design.md — ", "技术方案 — ", etc.
255
+ */
256
+ function extractTitle(text, rel, slug) {
257
+ const fm = /^---\s*\n([\s\S]*?)\n---/m.exec(text);
258
+ if (fm) {
259
+ const t = /^title:\s*"?([^"\n]+)"?/m.exec(fm[1]);
260
+ if (t) return cleanTitle(t[1].trim());
261
+ }
262
+ const h1 = /^#\s+(.+)$/m.exec(text);
263
+ if (h1) return cleanTitle(h1[1].replace(/<!--.*?-->/g, '').trim());
264
+ // Fallback: artifact basename (no suffix needed, buildFilename adds one)
265
+ return cleanTitle(path.basename(rel, '.md').replace(/[-_]/g, ' '));
266
+ }
267
+
268
+ /**
269
+ * Clean a raw title:
270
+ * 1. Strip leading "filename.ext — " or "keyword — " patterns
271
+ * (agents sometimes write "design.md — 实际标题" or "技术方案 — 实际标题")
272
+ * 2. Strip trailing " — " dangling dashes
273
+ */
274
+ function cleanTitle(raw) {
275
+ let t = raw;
276
+ // Remove any "*.md —" or "*.md -" fragment anywhere in the string
277
+ // Handles: "design.md — foo", "[slug] design.md — foo", etc.
278
+ t = t.replace(/\b\w[\w.-]*\.md\s*[—\-–]+\s*/gi, '');
279
+ // Strip leading bracket slug like "[18-xxx] "
280
+ t = t.replace(/^\s*\[[^\]]+\]\s*/, '');
281
+ // Strip trailing em-dash artifacts
282
+ t = t.replace(/\s*[—\-–]+\s*$/, '').trim();
283
+ return t || raw.trim();
284
+ }
285
+
286
+ /**
287
+ * Artifact type suffix — appended after the title, ARB style.
288
+ * design.md → " - 技术方案"
289
+ * incident-*.md → " - 故障复盘"
290
+ * regression*.md → " - 回归测试"
291
+ */
292
+ const ARTIFACT_SUFFIX = [
293
+ { match: /(^|\/)design\.md$/i, suffix: ' - 技术方案' },
294
+ { match: /(^|\/)requirement\.md$/i, suffix: ' - 需求分析' },
295
+ { match: /(^|\/)proposal\.md$/i, suffix: ' - 需求背景' },
296
+ { match: /(^|\/)incident[-_]?.*\.md$/i, suffix: ' - 故障复盘' },
297
+ { match: /(^|\/)regression.*\.md$/i, suffix: ' - 回归测试' },
298
+ ];
299
+
300
+ /**
301
+ * Build filename in ARB style:
302
+ * "[{slug}] {title} - {artifact-type}.md"
303
+ *
304
+ * Examples:
305
+ * design.md + title "健康保障会员升级版" → "[feature-18] 健康保障会员升级版 - 技术方案.md"
306
+ * knowledge/解决方案/foo.md → "[feature-18] foo.md" (keeps explicit knowledge filename)
307
+ */
308
+ function buildFilename(slug, title, rel) {
309
+ if (/^knowledge[\\/]/.test(rel)) return path.basename(rel);
310
+ const safetitle = title
311
+ .normalize('NFKC')
312
+ .replace(/[\\\/:*?"<>|\n\r\t]/g, ' ')
313
+ .replace(/\s{2,}/g, ' ')
314
+ .trim()
315
+ .slice(0, 60);
316
+
317
+ let suffix = '';
318
+ for (const r of ARTIFACT_SUFFIX) {
319
+ if (r.match.test(rel)) { suffix = r.suffix; break; }
320
+ }
321
+
322
+ // Avoid duplicate suffix if title already ends with the suffix text
323
+ const suffixLabel = suffix.replace(/^[\s-]+/, '');
324
+ const body = (suffixLabel && safetitle.endsWith(suffixLabel))
325
+ ? safetitle
326
+ : safetitle + suffix;
327
+
328
+ return `[${slug}] ${body}.md`;
329
+ }
330
+
331
+ async function buildEntry(root, body, category, filename, sourceRel, slug, { force = false } = {}) {
332
+ if (!body.trim()) return null;
333
+ // Default target: ~/.devflow/workspace/changes/<slug>/knowledge/<中文目录名>/<filename>
334
+ // Historical fallback remains <repo>/devflow/knowledge/ when callers provide old roots.
335
+ const dirName = categoryDir(category);
336
+ const targetAbs = path.join(resolveKnowledgeDir(root, slug), dirName, filename);
337
+ const newSha = sha256(body);
338
+ const action = await decideAction(targetAbs, newSha, force);
339
+ const kind = KIND_MAP[category] || 'note';
340
+ // Extract clean title from body for WeKnora display (readMarkdownFile uses title: frontmatter field)
341
+ const cleanedTitle = cleanTitle(extractTitle(body, sourceRel, slug));
342
+ return {
343
+ source: sourceRel,
344
+ title: cleanedTitle,
345
+ target: path.relative(root, targetAbs),
346
+ knowledgeRoot: resolveKnowledgeDir(root, slug),
347
+ targetAbs,
348
+ action,
349
+ sha256: newSha,
350
+ body,
351
+ category,
352
+ dirName,
353
+ filename,
354
+ kind,
355
+ };
356
+ }
357
+
358
+ function resolveKnowledgeDir(root, slug) {
359
+ return paths.workspaceKnowledgeDir(slug) || paths.knowledgeDir(root);
360
+ }
361
+
362
+ async function decideAction(targetAbs, newSha, force = false) {
363
+ if (!fsSync.existsSync(targetAbs)) return 'create';
364
+ if (force) return 'update';
365
+ try {
366
+ const existing = await fs.readFile(targetAbs, 'utf8');
367
+ const fmMatch = existing.match(/^sha256:\s*([a-f0-9]{64})/m);
368
+ const cmt = existing.match(/sha256=([a-f0-9]{12})/);
369
+ const stored = (fmMatch && fmMatch[1]) || (cmt && cmt[1]);
370
+ if (stored && newSha.startsWith(stored.slice(0, 12))) return 'noop';
371
+ } catch { /* ignore */ }
372
+ return 'update';
373
+ }
374
+
375
+ /**
376
+ * Write confirmed plan items to devflow/knowledge/ and update .meta.json.
377
+ * Exported so the CLI can call it after interactive confirmation.
378
+ */
379
+ async function executeItems(root, items, slug) {
380
+ for (const p of items) {
381
+ if (p.action === 'noop') continue;
382
+ await fs.mkdir(path.dirname(p.targetAbs), { recursive: true });
383
+ const fm = buildFrontmatter(p, slug);
384
+ await fs.writeFile(p.targetAbs, fm + p.body, 'utf8');
385
+ const catDir = path.dirname(p.targetAbs);
386
+ const meta = await ensureMeta(catDir, p.category);
387
+ meta.files = meta.files || {};
388
+ const prev = meta.files[p.filename] || {};
389
+ meta.files[p.filename] = {
390
+ ...prev,
391
+ sha256: p.sha256,
392
+ source: { slug, artifact: p.source },
393
+ updated_at: new Date().toISOString(),
394
+ };
395
+ await writeMeta(catDir, meta);
396
+ }
397
+ }
398
+
399
+ function buildFrontmatter(p, slug) {
400
+ return [
401
+ '---',
402
+ `kind: ${p.kind}`,
403
+ `slug: ${slug}`,
404
+ `title: ${p.title}`,
405
+ `source: ${p.source}`,
406
+ `category: ${p.category}`,
407
+ `sha256: ${p.sha256}`,
408
+ `deposited_at: ${new Date().toISOString()}`,
409
+ '---',
410
+ '',
411
+ ].join('\n');
412
+ }
413
+
414
+ module.exports = { deposit, executeItems, resolveChangeDir, extractTitle, buildFilename };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../core/paths.js');
6
+
7
+ /**
8
+ * Migrate a knowledge tree from one layout to another.
9
+ *
10
+ * Currently supported:
11
+ * from='arb-7' to='improved-9'
12
+ *
13
+ * The arb-7 layout (per arb-knowledge):
14
+ * 1-业务概念/ 2-业务规则/ 3-业务流程/ 4-接口/ 5-技术决策/ 6-排查记录/ 7-其他/
15
+ *
16
+ * Mapping table:
17
+ * 1-业务概念 → 1-domain/concepts
18
+ * 2-业务规则 → 1-domain/rules
19
+ * 3-业务流程 → 1-domain/scenarios
20
+ * 4-接口 → 2-system/contracts
21
+ * 5-技术决策 → 2-system/decisions
22
+ * 6-排查记录 → 3-ops/incidents
23
+ * 7-其他 → 4-archive/solutions
24
+ *
25
+ * Files are MOVED (not copied). A migration report is written to
26
+ * devflow/knowledge/.migration.log
27
+ *
28
+ * Options: { dryRun: bool, from: string, to: string }
29
+ */
30
+
31
+ const MAPPINGS = {
32
+ 'arb-7→improved-9': {
33
+ '1-业务概念': ['1-domain', 'concepts'],
34
+ '2-业务规则': ['1-domain', 'rules'],
35
+ '3-业务流程': ['1-domain', 'scenarios'],
36
+ '4-接口': ['2-system', 'contracts'],
37
+ '5-技术决策': ['2-system', 'decisions'],
38
+ '6-排查记录': ['3-ops', 'incidents'],
39
+ '7-其他': ['4-archive','solutions'],
40
+ // English fallbacks for ASCII forks of arb-knowledge
41
+ '1-concept': ['1-domain', 'concepts'],
42
+ '2-rule': ['1-domain', 'rules'],
43
+ '3-flow': ['1-domain', 'scenarios'],
44
+ '4-api': ['2-system', 'contracts'],
45
+ '5-decision': ['2-system', 'decisions'],
46
+ '6-incident': ['3-ops', 'incidents'],
47
+ '7-other': ['4-archive','solutions'],
48
+ },
49
+ };
50
+
51
+ async function migrate(root, opts = {}) {
52
+ const { dryRun = false, from = 'arb-7', to = 'improved-9' } = opts;
53
+ const key = `${from}→${to}`;
54
+ const map = MAPPINGS[key];
55
+ if (!map) throw new Error(`unsupported migration: ${key}`);
56
+
57
+ const kbDir = paths.knowledgeDir(root);
58
+ if (!fsSync.existsSync(kbDir)) throw new Error('no devflow/knowledge/ found');
59
+
60
+ const moved = [];
61
+ const skipped = [];
62
+ const conflicts = [];
63
+
64
+ const entries = await fs.readdir(kbDir, { withFileTypes: true });
65
+ for (const e of entries) {
66
+ if (!e.isDirectory()) continue;
67
+ if (!map[e.name]) {
68
+ // already migrated layout buckets (1-domain/2-system/...) or unknown
69
+ if (/^[1-4]-(domain|system|ops|archive)$/.test(e.name)) continue;
70
+ skipped.push({ name: e.name, reason: 'no mapping' });
71
+ continue;
72
+ }
73
+ const [domain, category] = map[e.name];
74
+ const srcDir = path.join(kbDir, e.name);
75
+ const dstDir = path.join(kbDir, domain, category);
76
+
77
+ if (!dryRun) await fs.mkdir(dstDir, { recursive: true });
78
+
79
+ const files = await walkAll(srcDir);
80
+ for (const f of files) {
81
+ const rel = path.relative(srcDir, f);
82
+ const dst = path.join(dstDir, rel.replace(/[\\\/]/g, '__'));
83
+ if (fsSync.existsSync(dst)) {
84
+ conflicts.push({ src: path.relative(root, f), dst: path.relative(root, dst) });
85
+ continue;
86
+ }
87
+ moved.push({ src: path.relative(root, f), dst: path.relative(root, dst) });
88
+ if (!dryRun) {
89
+ await fs.mkdir(path.dirname(dst), { recursive: true });
90
+ await fs.rename(f, dst);
91
+ }
92
+ }
93
+ if (!dryRun) {
94
+ // Try to rmdir source if now empty
95
+ await rmEmptyTree(srcDir);
96
+ }
97
+ }
98
+
99
+ const report = {
100
+ when: new Date().toISOString(),
101
+ from, to,
102
+ moved: moved.length,
103
+ skipped: skipped.length,
104
+ conflicts: conflicts.length,
105
+ details: { moved, skipped, conflicts },
106
+ };
107
+ if (!dryRun) {
108
+ await fs.writeFile(path.join(kbDir, '.migration.log'), JSON.stringify(report, null, 2) + '\n', 'utf8');
109
+ }
110
+ return report;
111
+ }
112
+
113
+ async function walkAll(dir) {
114
+ const out = [];
115
+ const stack = [dir];
116
+ while (stack.length) {
117
+ const cur = stack.pop();
118
+ const ents = await fs.readdir(cur, { withFileTypes: true });
119
+ for (const e of ents) {
120
+ const p = path.join(cur, e.name);
121
+ if (e.isDirectory()) stack.push(p);
122
+ else if (e.isFile() && !e.name.startsWith('.')) out.push(p);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+
128
+ async function rmEmptyTree(dir) {
129
+ if (!fsSync.existsSync(dir)) return;
130
+ const stack = [dir];
131
+ const stackForRemoval = [];
132
+ while (stack.length) {
133
+ const cur = stack.pop();
134
+ stackForRemoval.push(cur);
135
+ const ents = await fs.readdir(cur, { withFileTypes: true });
136
+ for (const e of ents) if (e.isDirectory()) stack.push(path.join(cur, e.name));
137
+ }
138
+ for (const d of stackForRemoval.reverse()) {
139
+ try {
140
+ const ents = await fs.readdir(d);
141
+ if (ents.length === 0 || (ents.length === 1 && ents[0] === '.meta.json')) {
142
+ if (ents.length === 1) await fs.unlink(path.join(d, '.meta.json'));
143
+ await fs.rmdir(d);
144
+ }
145
+ } catch { /* ignore */ }
146
+ }
147
+ }
148
+
149
+ module.exports = { migrate, MAPPINGS };