@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,219 @@
1
+ 'use strict';
2
+ /**
3
+ * Knowledge MR/PR mode.
4
+ *
5
+ * Two sub-modes:
6
+ *
7
+ * A) External KB repo (kb-git provider configured) — mirrors arb-workflow-kit:
8
+ * Clones/pulls the standalone knowledge GitLab/GitHub repo → copies files →
9
+ * creates branch + commit + push → creates MR/PR in THAT repo.
10
+ *
11
+ * B) Current project repo (no kb-git provider, or explicit --local-mr):
12
+ * Creates a branch in the current repo → commits devflow/knowledge/ →
13
+ * pushes → creates PR/MR via gh / glab / vcs provider.
14
+ *
15
+ * `createKnowledgePr` auto-detects which mode to use.
16
+ */
17
+
18
+ const cp = require('child_process');
19
+ const path = require('path');
20
+ const autodetect = require('../core/autodetect.js');
21
+ const loader = require('../providers/loader.js');
22
+
23
+ function git(args, cwd) {
24
+ return cp.spawnSync('git', args, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
25
+ }
26
+
27
+ function gitOrThrow(args, cwd, label) {
28
+ const r = git(args, cwd);
29
+ if (r.status !== 0) {
30
+ throw new Error(`[git ${label}] ${(r.stderr || r.stdout || '').trim()}`);
31
+ }
32
+ return (r.stdout || '').trim();
33
+ }
34
+
35
+ /**
36
+ * Main entry point — call after executeItems() has written knowledge files.
37
+ *
38
+ * Auto-selects mode:
39
+ * • External repo — when a kb provider with driver='git' is configured
40
+ * • Current repo — fallback (or when --local-mr is set)
41
+ *
42
+ * @param {string} root — git repo root
43
+ * @param {object} opts
44
+ * @param {string} slug — change slug
45
+ * @param {Array} items — plan items (used for PR body generation)
46
+ * @param {boolean} dryRun — preview only, no git/network ops
47
+ * @param {boolean} localMr — force current-repo mode even if kb-git is configured
48
+ * @returns {{ branch, url, baseBranch, status, mode, error? }}
49
+ */
50
+ async function createKnowledgePr(root, { slug, items, dryRun = false, localMr = false }) {
51
+ // ── Auto-detect: use external kb-git repo if configured ───────────────────
52
+ if (!localMr) {
53
+ let kbGitProvider = null;
54
+ try {
55
+ const all = await loader.listConfigured(root);
56
+ const gitEntry = all.find((e) => e.type === 'kb' && e.driver === 'git');
57
+ if (gitEntry) kbGitProvider = await loader.load(root, { name: gitEntry.name });
58
+ } catch { /* no kb-git configured → fall through */ }
59
+
60
+ if (kbGitProvider) {
61
+ const actionable = (items || []).filter((p) => p.action !== 'noop');
62
+ const result = await kbGitProvider.depositAndCreateMr(root, { items: actionable, slug, dryRun });
63
+ return { ...result, mode: 'external' };
64
+ }
65
+ }
66
+
67
+ // ── Mode B: current project repo ──────────────────────────────────────────
68
+ // Workspace knowledge lives under ~/.devflow/workspace and cannot be committed
69
+ // by the current project repository. In that case an external kb.git provider
70
+ // is required.
71
+ if ((items || []).some((p) => p.knowledgeRoot && !path.resolve(p.knowledgeRoot).startsWith(path.resolve(root) + path.sep))) {
72
+ return {
73
+ branch: null,
74
+ url: null,
75
+ baseBranch: null,
76
+ status: 'workspace_requires_kb_git',
77
+ mode: 'local',
78
+ error: 'workspace knowledge MR requires a kb.git provider; current project repo cannot commit files under ~/.devflow/workspace',
79
+ };
80
+ }
81
+
82
+ // ── 1. Determine branch names ──────────────────────────────────────────────
83
+ const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
84
+ const safeslug = slug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-{2,}/g, '-').slice(0, 40).replace(/-$/, '');
85
+ const branch = `knowledge/deposit-${safeslug}-${date}`;
86
+
87
+ let baseBranch;
88
+ try { baseBranch = autodetect.detectDefaultBranch(root); }
89
+ catch { baseBranch = 'main'; }
90
+
91
+ // ── 2. Check there are knowledge changes to commit ─────────────────────────
92
+ const statusOut = git(['status', '--porcelain', 'devflow/knowledge/'], root).stdout.trim();
93
+ if (!statusOut) {
94
+ return { branch: null, url: null, baseBranch, status: 'no_changes', mode: 'local' };
95
+ }
96
+
97
+ if (dryRun) {
98
+ return {
99
+ branch, baseBranch, url: null, status: 'dry_run', mode: 'local',
100
+ message: `would create branch '${branch}' from origin/${baseBranch} and open PR`,
101
+ };
102
+ }
103
+
104
+ // ── 3. Remember current branch ─────────────────────────────────────────────
105
+ const currentBranch = git(['rev-parse', '--abbrev-ref', 'HEAD'], root).stdout.trim() || 'HEAD';
106
+
107
+ let pushOk = false;
108
+ try {
109
+ // ── 4. Fetch origin (best-effort, don't fail if offline) ─────────────────
110
+ git(['fetch', 'origin', baseBranch, '--no-tags'], root);
111
+
112
+ // ── 5. Create branch from origin/<baseBranch> ─────────────────────────────
113
+ // git checkout -b <branch> origin/<base> preserves working-tree changes.
114
+ // If origin/<base> doesn't exist (local-only repo), fall back to HEAD.
115
+ const remoteRef = `origin/${baseBranch}`;
116
+ const remoteExists = git(['rev-parse', '--verify', '--quiet', remoteRef], root).status === 0;
117
+ const baseRef = remoteExists ? remoteRef : baseBranch;
118
+
119
+ gitOrThrow(['checkout', '-b', branch, baseRef], root, 'checkout -b');
120
+
121
+ // ── 6. Stage & commit ─────────────────────────────────────────────────────
122
+ gitOrThrow(['add', 'devflow/knowledge/'], root, 'add');
123
+
124
+ const actionable = (items || []).filter((p) => p.action !== 'noop');
125
+ const commitMsg = `chore(knowledge): deposit ${actionable.length} item(s) from ${slug}\n\n${buildCommitBody(actionable, slug)}`;
126
+
127
+ const commitR = git(['commit', '-m', commitMsg], root);
128
+ if (commitR.status !== 0) {
129
+ const out = (commitR.stdout + commitR.stderr).toLowerCase();
130
+ if (out.includes('nothing to commit')) {
131
+ await _returnBranch(root, currentBranch, branch);
132
+ return { branch, url: null, baseBranch, status: 'no_changes' };
133
+ }
134
+ throw new Error(`[git commit] ${(commitR.stderr || commitR.stdout || '').trim()}`);
135
+ }
136
+
137
+ // ── 7. Push ───────────────────────────────────────────────────────────────
138
+ gitOrThrow(['push', '-u', 'origin', branch], root, 'push');
139
+ pushOk = true;
140
+
141
+ // ── 8. Create PR/MR ───────────────────────────────────────────────────────
142
+ const prTitle = `[Knowledge] Deposit: ${slug}`;
143
+ const prBody = buildPrBody(actionable, slug, branch, baseBranch);
144
+
145
+ const vcs = await loader.load(root, { type: 'vcs' });
146
+ let url = null;
147
+ try {
148
+ const result = await vcs.pr({ title: prTitle, body_md: prBody, head: branch, base: baseBranch });
149
+ url = result.url;
150
+ } catch (prErr) {
151
+ // Branch is pushed — return partial success with the error.
152
+ await _returnBranch(root, currentBranch, branch);
153
+ return { branch, url: null, baseBranch, status: 'pushed_no_pr', error: prErr.message, mode: 'local' };
154
+ }
155
+
156
+ await _returnBranch(root, currentBranch, branch);
157
+ return { branch, url, baseBranch, status: 'created', mode: 'local' };
158
+
159
+ } catch (err) {
160
+ // Best-effort cleanup: return to original branch.
161
+ try { await _returnBranch(root, currentBranch, branch, !pushOk); } catch { /* ignore */ }
162
+ throw err;
163
+ }
164
+ }
165
+
166
+ // ── Helpers ─────────────────────────────────────────────────────────────────
167
+
168
+ async function _returnBranch(root, currentBranch, newBranch, deleteBranch = false) {
169
+ if (currentBranch && currentBranch !== newBranch && currentBranch !== 'HEAD') {
170
+ git(['checkout', currentBranch], root);
171
+ }
172
+ if (deleteBranch) {
173
+ git(['branch', '-D', newBranch], root);
174
+ }
175
+ }
176
+
177
+ function buildCommitBody(items, slug) {
178
+ const lines = [`Change: ${slug}`, ''];
179
+ for (const p of items) {
180
+ lines.push(`${p.action === 'create' ? 'A' : 'M'} ${p.target}`);
181
+ }
182
+ return lines.join('\n');
183
+ }
184
+
185
+ function buildPrBody(items, slug, branch, baseBranch) {
186
+ const lines = [
187
+ '## Knowledge Deposit',
188
+ '',
189
+ `**Change**: \`${slug}\``,
190
+ `**Branch**: \`${branch}\` → \`${baseBranch}\``,
191
+ '',
192
+ '## Changes',
193
+ '',
194
+ '| Op | File | Source | Confidence |',
195
+ '|----|------|--------|------------|',
196
+ ];
197
+
198
+ for (const p of items) {
199
+ const op = p.action === 'create' ? '+ New' : '~ Update';
200
+ const conf = (p.confidence || 0.5) >= 0.8 ? `${(p.confidence).toFixed(2)} ✓` : `${(p.confidence || 0.5).toFixed(2)} ⚠`;
201
+ const src = `${p.source}#${p.section}`;
202
+ lines.push(`| ${op} | \`${p.target}\` | ${src} | ${conf} |`);
203
+ }
204
+
205
+ lines.push(
206
+ '',
207
+ '## Review Checklist',
208
+ '',
209
+ '- [ ] 分类归属正确(domain/category 合理)',
210
+ '- [ ] 文件命名符合约定',
211
+ '- [ ] 内容与需求/设计一致,无敏感信息',
212
+ '- [ ] ⚠ confidence < 0.8 条目已人工确认分类',
213
+ '- [ ] 如有 `related:` 字段建议,已补充关联路径',
214
+ );
215
+
216
+ return lines.join('\n');
217
+ }
218
+
219
+ module.exports = { createKnowledgePr, buildPrBody };
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const path = require('path');
4
+ const paths = require('../core/paths.js');
5
+ const { walkKnowledge } = require('./registry.js');
6
+ const loader = require('../providers/loader.js');
7
+
8
+ /**
9
+ * Local + remote knowledge search.
10
+ *
11
+ * Scoring:
12
+ * per-keyword:
13
+ * +5 match in filename
14
+ * +3 match in H1/H2 heading
15
+ * +1 per match in body (capped at 10)
16
+ * normalized by sqrt(numKeywords).
17
+ *
18
+ * Returns top-K hits with snippet + path + score.
19
+ *
20
+ * Options:
21
+ * { keywords: [], k: 8, withRemote: bool }
22
+ */
23
+ async function query(root, opts = {}) {
24
+ const { keywords = [], k = 8, withRemote = true, slug = null } = opts;
25
+ if (!keywords.length) return { hits: [], remoteHits: [] };
26
+
27
+ const kws = keywords.map((s) => s.toLowerCase()).filter(Boolean);
28
+
29
+ // Local: specs first (project truth), then workspace/repo knowledge.
30
+ const all = [
31
+ ...(await walkSpecs(root)),
32
+ ...(slug ? await walkWorkspaceKnowledge(slug) : []),
33
+ ...(await walkKnowledge(root)),
34
+ ];
35
+ const localHits = [];
36
+ for (const f of all) {
37
+ const buf = await fs.readFile(f.abs, 'utf8');
38
+ const lower = buf.toLowerCase();
39
+ let score = 0;
40
+ let firstHitLine = -1;
41
+ let lineHit = '';
42
+ const fnameLower = f.file.toLowerCase();
43
+ const headings = (buf.match(/^#{1,2}\s+.+$/gm) || []).join('\n').toLowerCase();
44
+ for (const kw of kws) {
45
+ if (fnameLower.includes(kw)) score += 5;
46
+ if (headings.includes(kw)) score += 3;
47
+ const matches = countOccurrences(lower, kw);
48
+ score += Math.min(matches, 10);
49
+ if (firstHitLine === -1 && matches > 0) {
50
+ const lines = buf.split('\n');
51
+ for (let i = 0; i < lines.length; i++) {
52
+ if (lines[i].toLowerCase().includes(kw)) { firstHitLine = i + 1; lineHit = lines[i].trim(); break; }
53
+ }
54
+ }
55
+ }
56
+ score = +(score / Math.sqrt(kws.length)).toFixed(2);
57
+ if (score > 0) {
58
+ localHits.push({
59
+ path: path.relative(root, f.abs),
60
+ domain: f.domain, category: f.category, file: f.file, source: f.source || 'knowledge',
61
+ score, line: firstHitLine, snippet: lineHit.slice(0, 200),
62
+ });
63
+ }
64
+ }
65
+ localHits.sort((a, b) => sourcePriority(a.source) - sourcePriority(b.source) || b.score - a.score);
66
+
67
+ // Remote (if a kb provider configured & supports search)
68
+ let remoteHits = [];
69
+ if (withRemote) {
70
+ try {
71
+ const provider = await loader.load(root, { type: 'kb' });
72
+ if (typeof provider.search === 'function') {
73
+ remoteHits = await provider.search(kws.join(' '), { k }) || [];
74
+ }
75
+ } catch { /* ignore */ }
76
+ }
77
+
78
+ return { hits: localHits.slice(0, k), remoteHits: remoteHits.slice(0, k) };
79
+ }
80
+
81
+ async function walkSpecs(root) {
82
+ const dir = paths.specsDir(root);
83
+ const files = await walkMd(dir);
84
+ return files.map((abs) => ({
85
+ source: 'specs',
86
+ category: 'specs',
87
+ file: path.basename(abs),
88
+ abs,
89
+ }));
90
+ }
91
+
92
+ async function walkWorkspaceKnowledge(slug) {
93
+ const dir = paths.workspaceKnowledgeDir(slug);
94
+ const files = await walkMd(dir);
95
+ return files.map((abs) => ({
96
+ source: 'workspace-knowledge',
97
+ category: path.basename(path.dirname(abs)),
98
+ file: path.basename(abs),
99
+ abs,
100
+ }));
101
+ }
102
+
103
+ async function walkMd(dir) {
104
+ if (!dir || !require('fs').existsSync(dir)) return [];
105
+ const out = [];
106
+ const stack = [dir];
107
+ while (stack.length) {
108
+ const cur = stack.pop();
109
+ const ents = await fs.readdir(cur, { withFileTypes: true });
110
+ for (const e of ents) {
111
+ const p = path.join(cur, e.name);
112
+ if (e.isDirectory()) stack.push(p);
113
+ else if (e.isFile() && e.name.endsWith('.md')) out.push(p);
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function countOccurrences(haystack, needle) {
120
+ let count = 0; let pos = 0;
121
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) { count++; pos += needle.length; }
122
+ return count;
123
+ }
124
+
125
+ function sourcePriority(source) {
126
+ if (source === 'specs') return 0;
127
+ if (source === 'workspace-knowledge') return 1;
128
+ return 2;
129
+ }
130
+
131
+ module.exports = { query };
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const paths = require('../core/paths.js');
7
+ const { resolveTagInfo, BY_CHINESE_NAME } = require('./categories.js');
8
+
9
+ /**
10
+ * Per-category .meta.json acts as the file registry.
11
+ *
12
+ * Flat directory layout:
13
+ * ~/.devflow/workspace/knowledge/<category>/.meta.json
14
+ * ~/.devflow/workspace/changes/<slug>/knowledge/<category>/.meta.json
15
+ *
16
+ * Shape:
17
+ * {
18
+ * "category": "decisions",
19
+ * "tag_name": "架构决策",
20
+ * "tag_id": "", // filled in by sync
21
+ * "external": {},
22
+ * "files": { ... }
23
+ * }
24
+ */
25
+
26
+ async function readMeta(catDir) {
27
+ const file = path.join(catDir, '.meta.json');
28
+ if (!fsSync.existsSync(file)) return null;
29
+ try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch { return null; }
30
+ }
31
+
32
+ async function writeMeta(catDir, meta) {
33
+ const file = path.join(catDir, '.meta.json');
34
+ await fs.writeFile(file, JSON.stringify(meta, null, 2) + '\n', 'utf8');
35
+ }
36
+
37
+ /**
38
+ * Ensure a .meta.json exists in catDir.
39
+ *
40
+ * @param {string} catDir absolute path to the category directory
41
+ * @param {string} categoryId English slug OR Chinese dir name (e.g. "decisions" / "架构决策")
42
+ * @param {string} [_domainId] ignored — kept for call-site compatibility
43
+ */
44
+ async function ensureMeta(catDir, categoryId, _domainId) {
45
+ const existing = await readMeta(catDir);
46
+ if (existing) {
47
+ if (!existing.files) existing.files = {};
48
+ if (!existing.tag_name) {
49
+ const tagInfo = resolveTagInfo(categoryId);
50
+ existing.tag_name = tagInfo.name;
51
+ await writeMeta(catDir, existing);
52
+ }
53
+ return existing;
54
+ }
55
+ const tagInfo = resolveTagInfo(categoryId);
56
+ // Store the Chinese name as category so it's human-readable in .meta.json
57
+ const m = {
58
+ $schema: 'https://devflow.dev/schemas/meta.schema.json',
59
+ category: tagInfo.name, // e.g. "架构决策"
60
+ tag_name: tagInfo.name,
61
+ tag_id: '',
62
+ external: {},
63
+ files: {},
64
+ };
65
+ await writeMeta(catDir, m);
66
+ return m;
67
+ }
68
+
69
+ function sha256(buf) {
70
+ const h = crypto.createHash('sha256');
71
+ h.update(buf);
72
+ return h.digest('hex');
73
+ }
74
+
75
+ /**
76
+ * Walk all knowledge trees and yield { root, dir, category, file, abs }.
77
+ *
78
+ * Supports BOTH the new flat layout (knowledge/<category>/)
79
+ * AND the legacy nested layout (knowledge/<domain>/<category>/)
80
+ * so old projects can migrate gradually.
81
+ */
82
+ async function walkKnowledge(root, opts = {}) {
83
+ const out = [];
84
+ for (const dir of knowledgeRoots(root, opts)) {
85
+ await walkKnowledgeRoot(dir, out);
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function knowledgeRoots(root, opts = {}) {
91
+ const dirs = [];
92
+ const add = (dir) => {
93
+ if (dir && fsSync.existsSync(dir) && !dirs.includes(dir)) dirs.push(dir);
94
+ };
95
+
96
+ if (opts.slug) {
97
+ add(paths.workspaceKnowledgeDir(opts.slug));
98
+ add(path.join(paths.devflowDir(root), 'changes', opts.slug, 'knowledge'));
99
+ return dirs;
100
+ }
101
+
102
+ add(paths.workspaceKnowledgeRoot());
103
+
104
+ const changesDir = paths.changesDir(root);
105
+ if (fsSync.existsSync(changesDir)) {
106
+ const entries = fsSync.readdirSync(changesDir, { withFileTypes: true });
107
+ for (const e of entries) {
108
+ if (e.isDirectory()) add(path.join(changesDir, e.name, 'knowledge'));
109
+ }
110
+ }
111
+
112
+ // Legacy project-local knowledge remains readable for older projects.
113
+ add(paths.knowledgeDir(root));
114
+ return dirs;
115
+ }
116
+
117
+ async function walkKnowledgeRoot(dir, out) {
118
+ const entries = await fs.readdir(dir, { withFileTypes: true });
119
+
120
+ for (const e of entries) {
121
+ if (!e.isDirectory()) continue;
122
+ const ePath = path.join(dir, e.name);
123
+ // Check if this is a flat category dir (has .meta.json or .md files directly)
124
+ const sub = await fs.readdir(ePath, { withFileTypes: true });
125
+ const hasMd = sub.some((s) => s.isFile() && s.name.endsWith('.md'));
126
+ const hasMeta = sub.some((s) => s.isFile() && s.name === '.meta.json');
127
+ const hasSubDirs = sub.some((s) => s.isDirectory());
128
+
129
+ if (hasMd || hasMeta) {
130
+ // Flat category dir
131
+ for (const s of sub) {
132
+ if (s.isFile() && s.name.endsWith('.md')) {
133
+ out.push({ root: dir, dir: ePath, category: e.name, file: s.name, abs: path.join(ePath, s.name) });
134
+ }
135
+ }
136
+ } else if (hasSubDirs) {
137
+ // Legacy nested domain/category layout
138
+ for (const c of sub) {
139
+ if (!c.isDirectory()) continue;
140
+ const cs = path.join(ePath, c.name);
141
+ const files = await fs.readdir(cs);
142
+ for (const f of files) {
143
+ if (!f.endsWith('.md')) continue;
144
+ out.push({ root: dir, dir: cs, category: c.name, file: f, abs: path.join(cs, f) });
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ module.exports = { readMeta, writeMeta, ensureMeta, sha256, walkKnowledge, knowledgeRoots };
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const loader = require('../providers/loader.js');
6
+ const { walkKnowledge, knowledgeRoots, readMeta, writeMeta, sha256 } = require('./registry.js');
7
+ const { buildTagMap, resolveTagInfo } = require('./categories.js');
8
+
9
+ /**
10
+ * Push knowledge files to the configured kb provider, incrementally.
11
+ *
12
+ * Strategy:
13
+ * 1. walk knowledge/ tree → collect (cat, file, sha256)
14
+ * 2. read each category's .meta.json → previously synced sha256 + uuid
15
+ * 3. diff:
16
+ * new file → provider.upload → store uuid+sha
17
+ * sha changed → provider.update
18
+ * file deleted → provider.delete (if registry had uuid)
19
+ * unchanged → noop
20
+ * 4. write updated .meta.json
21
+ *
22
+ * Options:
23
+ * { dryRun, force, providerName, slug }
24
+ * force=true → clear all stored UUIDs/sha before diff,
25
+ * so every file is treated as new (re-uploads all).
26
+ * Use after fixing an API misconfiguration that caused empty uploads.
27
+ *
28
+ * Returns { actions: [...], summary: { create, update, delete, noop } }
29
+ */
30
+ async function sync(root, opts = {}) {
31
+ const { dryRun = false, force = false, providerName, slug } = opts;
32
+ const provider = await loader.load(root, providerName ? { name: providerName } : { type: 'kb' });
33
+
34
+ const summary = { create: 0, update: 0, delete: 0, noop: 0, tag: 0, scanned: 0 };
35
+ const actions = [];
36
+ const tagMap = buildTagMap((provider.config && provider.config.tags) || {});
37
+ // Collect { uuid → tagId } for docs that exist but were never tagged (or tag changed)
38
+ const pendingTagUpdates = {};
39
+
40
+ // ── Pre-create ALL known tags before uploading any documents ──────────────
41
+ // This ensures tags exist and tag_ids are cached before the first upload,
42
+ // so every document gets tagged immediately at creation time.
43
+ if (typeof provider.ensureTag === 'function') {
44
+ for (const [_catSlug, tagInfo] of Object.entries(tagMap)) {
45
+ try {
46
+ await provider.ensureTag(tagInfo.name, tagInfo.color, tagInfo.order);
47
+ } catch { /* non-fatal: continue without this tag */ }
48
+ }
49
+ }
50
+
51
+ // Build current view per category (from disk).
52
+ // Flat layout: key = category slug (e.g. "decisions")
53
+ const all = await walkKnowledge(root, { slug });
54
+ const byCat = {}; // dir -> { key, dir, files: { name -> { abs, sha } } }
55
+ for (const f of all) {
56
+ const dir = f.dir;
57
+ const key = f.category;
58
+ if (!byCat[dir]) byCat[dir] = { key, dir, files: {} };
59
+ const buf = await fs.readFile(f.abs);
60
+ byCat[dir].files[f.file] = { abs: f.abs, sha: sha256(buf), buf };
61
+ summary.scanned++;
62
+ }
63
+
64
+ // Also include dirs that have a .meta.json but no files (detect deletions).
65
+ for (const kbDir of knowledgeRoots(root, { slug })) {
66
+ const entries = await fs.readdir(kbDir, { withFileTypes: true });
67
+ for (const e of entries) {
68
+ if (!e.isDirectory()) continue;
69
+ const eDir = path.join(kbDir, e.name);
70
+ if (fsSync.existsSync(path.join(eDir, '.meta.json'))) {
71
+ if (!byCat[eDir]) byCat[eDir] = { key: e.name, dir: eDir, files: {} };
72
+ }
73
+ }
74
+ }
75
+ // key is now the Chinese directory name (e.g. "架构决策") or English slug for legacy dirs
76
+
77
+ for (const catDir of Object.keys(byCat)) {
78
+ const { key, dir, files } = byCat[catDir];
79
+ let meta = (await readMeta(dir)) || { category: path.basename(dir), files: {}, external: {} };
80
+ meta.files = meta.files || {};
81
+
82
+ // --force: clear all stored UUIDs/sha so every file is re-uploaded as new
83
+ if (force) {
84
+ for (const name of Object.keys(meta.files)) {
85
+ delete meta.files[name].uuid;
86
+ delete meta.files[name].sha256;
87
+ }
88
+ }
89
+
90
+ // ── Resolve tag_id for this category ─────────────────────────────────
91
+ // All tags were pre-created above; here we just look them up from cache.
92
+ // key is category slug (e.g. "decisions").
93
+ const tagInfo = resolveTagInfo(key, null, tagMap);
94
+ let tagId = (meta.tag_name === tagInfo.name) ? (meta.tag_id || '') : '';
95
+ if (!tagId && typeof provider.ensureTag === 'function') {
96
+ try {
97
+ const tag = await provider.ensureTag(tagInfo.name, tagInfo.color, tagInfo.order);
98
+ if (tag && tag.id) {
99
+ tagId = tag.id;
100
+ meta.tag_id = tag.id;
101
+ meta.tag_name = tag.name;
102
+ }
103
+ } catch (e) {
104
+ actions.push({ kind: 'warn', dir: key, error: `ensureTag failed: ${e.message}` });
105
+ }
106
+ }
107
+
108
+ // create / update
109
+ for (const name of Object.keys(files)) {
110
+ const cur = files[name];
111
+ const reg = meta.files[name] || {};
112
+ const uploadMeta = { category: meta.category, filename: name, tag_id: tagId };
113
+
114
+ if (reg.uuid && reg.sha256 === cur.sha) {
115
+ // Content unchanged — but check if tag needs to be applied retroactively.
116
+ // A doc needs tag patching when:
117
+ // - we now have a tagId
118
+ // - the stored tag_id doesn't match (including previously untagged docs)
119
+ if (tagId && reg.tag_id !== tagId) {
120
+ pendingTagUpdates[reg.uuid] = tagId;
121
+ meta.files[name] = { ...reg, tag_id: tagId };
122
+ actions.push({ kind: 'tag', name, dir: key, uuid: reg.uuid });
123
+ summary.tag++;
124
+ } else {
125
+ summary.noop++;
126
+ actions.push({ kind: 'noop', name, dir: key });
127
+ }
128
+ continue;
129
+ }
130
+
131
+ if (!reg.uuid) {
132
+ actions.push({ kind: 'create', name, dir: key });
133
+ if (!dryRun) {
134
+ const r = await provider.upload(cur.abs, uploadMeta);
135
+ meta.files[name] = { ...reg, uuid: r.uuid, url: r.url || null, sha256: cur.sha, tag_id: tagId || null, synced_at: now() };
136
+ }
137
+ summary.create++;
138
+ } else {
139
+ actions.push({ kind: 'update', name, dir: key, uuid: reg.uuid });
140
+ if (!dryRun) {
141
+ await provider.update(reg.uuid, cur.abs, uploadMeta);
142
+ meta.files[name] = { ...reg, sha256: cur.sha, tag_id: tagId || null, synced_at: now() };
143
+ }
144
+ summary.update++;
145
+ }
146
+ }
147
+
148
+ // delete: in registry but not on disk
149
+ for (const name of Object.keys(meta.files)) {
150
+ if (files[name]) continue;
151
+ const reg = meta.files[name];
152
+ if (!reg || !reg.uuid) { delete meta.files[name]; continue; }
153
+ actions.push({ kind: 'delete', name, dir: key, uuid: reg.uuid });
154
+ if (!dryRun) {
155
+ try { await provider.delete(reg.uuid); } catch (e) { actions[actions.length-1].error = e.message; }
156
+ delete meta.files[name];
157
+ }
158
+ summary.delete++;
159
+ }
160
+
161
+ if (!dryRun) await writeMeta(dir, meta);
162
+ }
163
+
164
+ // ── Batch-apply tags for docs that were previously untagged ─────────────
165
+ if (!dryRun && Object.keys(pendingTagUpdates).length > 0
166
+ && typeof provider.batchUpdateTags === 'function') {
167
+ try {
168
+ await provider.batchUpdateTags(pendingTagUpdates);
169
+ } catch (e) {
170
+ actions.push({ kind: 'warn', error: `batchUpdateTags failed: ${e.message}` });
171
+ }
172
+ }
173
+
174
+ return { actions, summary };
175
+ }
176
+
177
+ function now() { return new Date().toISOString(); }
178
+
179
+ module.exports = { sync };