@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.
- package/CHANGELOG.md +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
const paths = require('./paths.js');
|
|
7
|
+
const stateMod = require('./state.js');
|
|
8
|
+
const autodetect = require('./autodetect.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* worktree-swarm: per-task git worktree isolation for the apply phase.
|
|
12
|
+
*
|
|
13
|
+
* Layout (under repo root):
|
|
14
|
+
* .devflow-worktrees/<slug>/ — single worktree (no --task)
|
|
15
|
+
* .devflow-worktrees/<slug>/task-<id>/ — per-task worktrees
|
|
16
|
+
*
|
|
17
|
+
* Branch naming:
|
|
18
|
+
* devflow/<slug> — single
|
|
19
|
+
* devflow/<slug>/task-<id> — per-task
|
|
20
|
+
*
|
|
21
|
+
* State (changes/<slug>/state.json):
|
|
22
|
+
* phases.apply.tasks: [{ id, title, worktree, branch, status, started_at, ended_at, notes[] }]
|
|
23
|
+
*
|
|
24
|
+
* Status lifecycle: pending → in_progress → done (or skipped/failed)
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
function git(args, cwd) {
|
|
28
|
+
return cp.spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isGitRepo(root) {
|
|
32
|
+
if (!fsSync.existsSync(path.join(root, '.git'))) return false;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureTaskBucket(state) {
|
|
37
|
+
state.phases = state.phases || {};
|
|
38
|
+
state.phases.apply = state.phases.apply || { status: 'pending' };
|
|
39
|
+
state.phases.apply.tasks = state.phases.apply.tasks || [];
|
|
40
|
+
state.phases.apply.iterations = state.phases.apply.iterations || 0;
|
|
41
|
+
return state.phases.apply;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findTask(state, taskId) {
|
|
45
|
+
const apply = ensureTaskBucket(state);
|
|
46
|
+
return apply.tasks.find((t) => t.id === taskId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function branchNameFor(slug, taskId) {
|
|
50
|
+
return taskId ? `devflow/${slug}/${taskId}` : `devflow/${slug}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function worktreeFor(root, slug, taskId) {
|
|
54
|
+
return paths.worktreePath(root, slug, taskId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a worktree for a task.
|
|
59
|
+
* Idempotent: if the worktree already exists, returns existing record.
|
|
60
|
+
*/
|
|
61
|
+
async function create(root, slug, opts = {}) {
|
|
62
|
+
if (!isGitRepo(root)) throw new Error('not a git repo — cannot create worktree');
|
|
63
|
+
const taskId = normaliseTaskId(opts.task);
|
|
64
|
+
const wtPath = worktreeFor(root, slug, taskId);
|
|
65
|
+
const branch = opts.branch || branchNameFor(slug, taskId);
|
|
66
|
+
// Base resolution: explicit opt > current checked-out branch (normal case
|
|
67
|
+
// when user runs `devflow apply` from their feature branch) > autodetected
|
|
68
|
+
// default branch (covers repos that use `master` instead of `main`).
|
|
69
|
+
const base = opts.base || currentBranch(root) || autodetect.detectDefaultBranch(root);
|
|
70
|
+
const title = opts.title || (taskId ? `task ${taskId}` : 'main');
|
|
71
|
+
|
|
72
|
+
const state = await stateMod.read(root, slug);
|
|
73
|
+
const apply = ensureTaskBucket(state);
|
|
74
|
+
let task = findTask(state, taskId || 'main');
|
|
75
|
+
|
|
76
|
+
// Already exists?
|
|
77
|
+
if (fsSync.existsSync(wtPath)) {
|
|
78
|
+
if (!task) {
|
|
79
|
+
task = recordTask(apply, taskId || 'main', { title, worktree: wtPath, branch });
|
|
80
|
+
await stateMod.write(root, slug, state);
|
|
81
|
+
}
|
|
82
|
+
return { created: false, path: wtPath, branch, task };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Ensure parent dir
|
|
86
|
+
await fs.mkdir(path.dirname(wtPath), { recursive: true });
|
|
87
|
+
|
|
88
|
+
// Create branch if missing
|
|
89
|
+
const branchExists = git(['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], root).status === 0;
|
|
90
|
+
const args = ['worktree', 'add'];
|
|
91
|
+
if (!branchExists) args.push('-b', branch);
|
|
92
|
+
args.push(wtPath);
|
|
93
|
+
if (!branchExists) args.push(base);
|
|
94
|
+
else args.push(branch);
|
|
95
|
+
|
|
96
|
+
const r = git(args, root);
|
|
97
|
+
if (r.status !== 0) {
|
|
98
|
+
throw new Error(`git worktree add failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
task = recordTask(apply, taskId || 'main', { title, worktree: wtPath, branch });
|
|
102
|
+
apply.status = apply.status === 'done' ? 'in_progress' : (apply.status || 'in_progress');
|
|
103
|
+
if (state.currentPhase === 'plan' || state.currentPhase === 'review') {
|
|
104
|
+
state.currentPhase = 'apply';
|
|
105
|
+
}
|
|
106
|
+
stateMod.logEvent(state, 'worktree.create', { task: task.id, path: wtPath, branch });
|
|
107
|
+
await stateMod.write(root, slug, state);
|
|
108
|
+
|
|
109
|
+
return { created: true, path: wtPath, branch, task };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function recordTask(apply, id, fields) {
|
|
113
|
+
const now = new Date().toISOString();
|
|
114
|
+
const existing = apply.tasks.find((t) => t.id === id);
|
|
115
|
+
if (existing) {
|
|
116
|
+
Object.assign(existing, fields, { updated_at: now });
|
|
117
|
+
return existing;
|
|
118
|
+
}
|
|
119
|
+
const t = {
|
|
120
|
+
id,
|
|
121
|
+
title: fields.title || id,
|
|
122
|
+
worktree: fields.worktree,
|
|
123
|
+
branch: fields.branch,
|
|
124
|
+
status: 'pending',
|
|
125
|
+
started_at: now,
|
|
126
|
+
notes: [],
|
|
127
|
+
};
|
|
128
|
+
apply.tasks.push(t);
|
|
129
|
+
return t;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* List all worktrees for a slug, or for all slugs if slug omitted.
|
|
134
|
+
* Cross-references git's own worktree list with our state.json records.
|
|
135
|
+
*/
|
|
136
|
+
async function list(root, slug) {
|
|
137
|
+
const gitList = parseGitWorktrees(root);
|
|
138
|
+
const out = [];
|
|
139
|
+
if (slug) {
|
|
140
|
+
const state = await stateMod.read(root, slug).catch(() => null);
|
|
141
|
+
const apply = state ? ensureTaskBucket(state) : { tasks: [] };
|
|
142
|
+
for (const t of apply.tasks) {
|
|
143
|
+
out.push({
|
|
144
|
+
slug,
|
|
145
|
+
task: t.id,
|
|
146
|
+
path: t.worktree,
|
|
147
|
+
branch: t.branch,
|
|
148
|
+
status: t.status,
|
|
149
|
+
gitTracked: gitList.some((g) => g.path === t.worktree),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
// All slugs
|
|
155
|
+
const changesDir = paths.changesDir(root);
|
|
156
|
+
if (!fsSync.existsSync(changesDir)) return [];
|
|
157
|
+
const slugs = (await fs.readdir(changesDir)).filter((d) =>
|
|
158
|
+
fsSync.statSync(path.join(changesDir, d)).isDirectory()
|
|
159
|
+
);
|
|
160
|
+
for (const s of slugs) {
|
|
161
|
+
const inner = await list(root, s);
|
|
162
|
+
out.push(...inner);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseGitWorktrees(root) {
|
|
168
|
+
const r = git(['worktree', 'list', '--porcelain'], root);
|
|
169
|
+
if (r.status !== 0) return [];
|
|
170
|
+
const out = [];
|
|
171
|
+
let cur = null;
|
|
172
|
+
for (const line of (r.stdout || '').split('\n')) {
|
|
173
|
+
if (line.startsWith('worktree ')) {
|
|
174
|
+
if (cur) out.push(cur);
|
|
175
|
+
cur = { path: line.slice('worktree '.length).trim() };
|
|
176
|
+
} else if (line.startsWith('branch ')) {
|
|
177
|
+
cur && (cur.branch = line.slice('branch '.length).trim());
|
|
178
|
+
} else if (line.startsWith('HEAD ')) {
|
|
179
|
+
cur && (cur.head = line.slice('HEAD '.length).trim());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (cur) out.push(cur);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Cleanup a worktree (and optionally its branch).
|
|
188
|
+
*/
|
|
189
|
+
async function cleanup(root, slug, opts = {}) {
|
|
190
|
+
const taskId = normaliseTaskId(opts.task) || 'main';
|
|
191
|
+
const state = await stateMod.read(root, slug).catch(() => null);
|
|
192
|
+
if (!state) throw new Error(`no state for slug: ${slug}`);
|
|
193
|
+
const apply = ensureTaskBucket(state);
|
|
194
|
+
const idx = apply.tasks.findIndex((t) => t.id === taskId);
|
|
195
|
+
if (idx === -1) throw new Error(`no task recorded: ${taskId}`);
|
|
196
|
+
const task = apply.tasks[idx];
|
|
197
|
+
|
|
198
|
+
// git worktree remove (force if dirty unless --no-force)
|
|
199
|
+
const removeArgs = ['worktree', 'remove'];
|
|
200
|
+
if (opts.force !== false) removeArgs.push('--force');
|
|
201
|
+
removeArgs.push(task.worktree);
|
|
202
|
+
const r = git(removeArgs, root);
|
|
203
|
+
// Tolerant: if path doesn't exist anymore, prune and continue.
|
|
204
|
+
if (r.status !== 0 && !/is not a working tree/.test(r.stderr || '')) {
|
|
205
|
+
git(['worktree', 'prune'], root);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Delete branch unless asked to keep
|
|
209
|
+
if (!opts.keepBranch) {
|
|
210
|
+
git(['branch', '-D', task.branch], root);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
apply.tasks.splice(idx, 1);
|
|
214
|
+
stateMod.logEvent(state, 'worktree.cleanup', { task: taskId, branch: task.branch, kept: !!opts.keepBranch });
|
|
215
|
+
await stateMod.write(root, slug, state);
|
|
216
|
+
|
|
217
|
+
return { removed: true, branch: task.branch, kept: !!opts.keepBranch };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function currentBranch(root) {
|
|
221
|
+
const r = git(['rev-parse', '--abbrev-ref', 'HEAD'], root);
|
|
222
|
+
if (r.status !== 0) return null;
|
|
223
|
+
const b = (r.stdout || '').trim();
|
|
224
|
+
return b === 'HEAD' ? null : b;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normaliseTaskId(t) {
|
|
228
|
+
if (t === undefined || t === null || t === '' || t === false) return null;
|
|
229
|
+
const s = String(t);
|
|
230
|
+
if (/^\d+$/.test(s)) return `task-${s}`;
|
|
231
|
+
if (/^T-0*\d+$/i.test(s)) return `task-${parseInt(s.slice(2), 10)}`;
|
|
232
|
+
if (/^task-/.test(s)) return s;
|
|
233
|
+
return s.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = {
|
|
237
|
+
create, list, cleanup,
|
|
238
|
+
branchNameFor, worktreeFor, normaliseTaskId,
|
|
239
|
+
parseGitWorktrees, currentBranch,
|
|
240
|
+
findTask, ensureTaskBucket,
|
|
241
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical mapping: English slug → WeKnora tag info.
|
|
5
|
+
*
|
|
6
|
+
* `name` is BOTH the WeKnora tag label AND the local directory name:
|
|
7
|
+
* devflow/knowledge/
|
|
8
|
+
* 架构决策/ 接口契约/ 服务信息/
|
|
9
|
+
* 领域概念/ 业务规则/ 业务场景/
|
|
10
|
+
* 环境配置/ 故障复盘/ 运维手册/
|
|
11
|
+
* 解决方案/
|
|
12
|
+
*
|
|
13
|
+
* Colors follow WeKnora palette; grouped by domain tier:
|
|
14
|
+
* domain → blue (#1890ff)
|
|
15
|
+
* system → green (#52c41a)
|
|
16
|
+
* ops → orange (#fa8c16)
|
|
17
|
+
* archive → gray (#8c8c8c)
|
|
18
|
+
*/
|
|
19
|
+
const CATEGORY_TAG_NAMES = {
|
|
20
|
+
// ── domain ──────────────────────────────────────────
|
|
21
|
+
concepts: { name: '领域概念', color: '#1890ff', order: 10 },
|
|
22
|
+
rules: { name: '业务规则', color: '#096dd9', order: 11 },
|
|
23
|
+
scenarios: { name: '业务场景', color: '#40a9ff', order: 12 },
|
|
24
|
+
|
|
25
|
+
// ── system ──────────────────────────────────────────
|
|
26
|
+
contracts: { name: '接口契约', color: '#52c41a', order: 20 },
|
|
27
|
+
decisions: { name: '架构决策', color: '#389e0d', order: 21 },
|
|
28
|
+
services: { name: '服务信息', color: '#73d13d', order: 22 },
|
|
29
|
+
|
|
30
|
+
// ── ops ─────────────────────────────────────────────
|
|
31
|
+
environments: { name: '环境配置', color: '#fa8c16', order: 30 },
|
|
32
|
+
incidents: { name: '故障复盘', color: '#d4380d', order: 31 },
|
|
33
|
+
runbooks: { name: '运维手册', color: '#ffa940', order: 32 },
|
|
34
|
+
|
|
35
|
+
// ── archive ─────────────────────────────────────────
|
|
36
|
+
solutions: { name: '解决方案', color: '#8c8c8c', order: 40 },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Reverse map: Chinese name → CATEGORY_TAG_NAMES entry (for directory-based lookups). */
|
|
40
|
+
const BY_CHINESE_NAME = Object.fromEntries(
|
|
41
|
+
Object.values(CATEGORY_TAG_NAMES).map((v) => [v.name, v]),
|
|
42
|
+
);
|
|
43
|
+
const SLUG_BY_CHINESE_NAME = Object.fromEntries(
|
|
44
|
+
Object.entries(CATEGORY_TAG_NAMES).map(([slug, v]) => [v.name, slug]),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const DEFAULT_COLOR = '#bfbfbf';
|
|
48
|
+
const DEFAULT_ORDER = 99;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve tag info for a given category identifier.
|
|
52
|
+
*
|
|
53
|
+
* Accepts:
|
|
54
|
+
* - English slug: "decisions", "contracts"
|
|
55
|
+
* - Legacy domain/category: "2-system/decisions"
|
|
56
|
+
* - Chinese directory name: "架构决策", "接口契约"
|
|
57
|
+
*
|
|
58
|
+
* @param {string} categoryId
|
|
59
|
+
* @param {string} [tagNameHint] optional override from .meta.json tag_name
|
|
60
|
+
*/
|
|
61
|
+
function buildTagMap(overrides = {}) {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [slug, info] of Object.entries(CATEGORY_TAG_NAMES)) {
|
|
64
|
+
out[slug] = { ...info, ...(overrides[slug] || {}) };
|
|
65
|
+
}
|
|
66
|
+
for (const [slug, info] of Object.entries(overrides || {})) {
|
|
67
|
+
if (!out[slug]) out[slug] = {
|
|
68
|
+
name: info.name || slug,
|
|
69
|
+
color: info.color || DEFAULT_COLOR,
|
|
70
|
+
order: info.order || DEFAULT_ORDER,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveTagInfo(categoryId, tagNameHint, tagMap = CATEGORY_TAG_NAMES) {
|
|
77
|
+
// Strip legacy domain prefix
|
|
78
|
+
const slug = (typeof categoryId === 'string' && categoryId.includes('/'))
|
|
79
|
+
? categoryId.split('/').pop()
|
|
80
|
+
: categoryId;
|
|
81
|
+
|
|
82
|
+
// English slug lookup
|
|
83
|
+
const bySlug = tagMap[slug];
|
|
84
|
+
if (bySlug) {
|
|
85
|
+
return { ...bySlug };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Chinese name lookup (used when walking directories named after Chinese tags).
|
|
89
|
+
// Resolve through the canonical slug first so provider-specific tag overrides apply.
|
|
90
|
+
const canonicalSlug = SLUG_BY_CHINESE_NAME[slug];
|
|
91
|
+
if (canonicalSlug && tagMap[canonicalSlug]) return { ...tagMap[canonicalSlug] };
|
|
92
|
+
if (canonicalSlug && BY_CHINESE_NAME[slug]) return { ...BY_CHINESE_NAME[slug] };
|
|
93
|
+
|
|
94
|
+
if (tagNameHint) return { name: tagNameHint, color: DEFAULT_COLOR, order: DEFAULT_ORDER };
|
|
95
|
+
return { name: slug, color: DEFAULT_COLOR, order: DEFAULT_ORDER };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Return the directory name for a given English category slug.
|
|
100
|
+
* e.g. categoryDir('decisions') → '架构决策'
|
|
101
|
+
*/
|
|
102
|
+
function categoryDir(slug) {
|
|
103
|
+
const entry = CATEGORY_TAG_NAMES[slug];
|
|
104
|
+
return entry ? entry.name : slug;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { CATEGORY_TAG_NAMES, BY_CHINESE_NAME, buildTagMap, resolveTagInfo, categoryDir };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Rules-based classifier: given an extracted knowledge entry from a change folder,
|
|
4
|
+
* decide which (domain, category) under devflow/knowledge/ it belongs to.
|
|
5
|
+
*
|
|
6
|
+
* Inputs:
|
|
7
|
+
* {
|
|
8
|
+
* artifact: string // 'knowledge/<category>/*.md' | 'design.md' | 'reports/incident-*.md' | ...
|
|
9
|
+
* section: string // section heading
|
|
10
|
+
* content: string // raw markdown
|
|
11
|
+
* hints: object // optional override: { domain, category }
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* Output:
|
|
15
|
+
* { domain, category, suggestedFilename, confidence, reasons[] }
|
|
16
|
+
*
|
|
17
|
+
* Order of precedence:
|
|
18
|
+
* 1. explicit hint
|
|
19
|
+
* 2. artifact-based mapping
|
|
20
|
+
* 3. section/keyword heuristics
|
|
21
|
+
* 4. default to '4-archive/solutions'
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ARTIFACT_RULES = [
|
|
25
|
+
// Incidents and runbooks live under ops.
|
|
26
|
+
{ match: /(^|\/)incident[-_]?.*\.md$/i, domain: '3-ops', category: 'incidents' },
|
|
27
|
+
{ match: /(^|\/)runbook|sop[-_]?/i, domain: '3-ops', category: 'runbooks' },
|
|
28
|
+
// Design / ADR and contracts live under system.
|
|
29
|
+
{ match: /(^|\/)adr[-_]?\d*/i, domain: '2-system', category: 'decisions' },
|
|
30
|
+
{ match: /(^|\/)contract[-_]|api[-_]?spec/i, domain: '2-system', category: 'contracts' },
|
|
31
|
+
{ match: /(^|\/)design\.md$/i, domain: '2-system', category: 'decisions' },
|
|
32
|
+
// Brainstorm / requirements / proposal -> domain
|
|
33
|
+
{ match: /(^|\/)proposal\.md$/i, domain: '1-domain', category: 'scenarios' },
|
|
34
|
+
{ match: /(^|\/)requirement\.md$/i, domain: '1-domain', category: 'scenarios' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const SECTION_HINTS = [
|
|
38
|
+
{ re: /(术语|名词|glossary|terminology)/i, domain: '1-domain', category: 'concepts' },
|
|
39
|
+
{ re: /(规则|决策表|policy|rule)/i, domain: '1-domain', category: 'rules' },
|
|
40
|
+
{ re: /(业务流程|场景|scenario|story)/i, domain: '1-domain', category: 'scenarios' },
|
|
41
|
+
{ re: /(api|接口|契约|contract|protobuf|grpc|rest|kafka|event)/i, domain: '2-system', category: 'contracts' },
|
|
42
|
+
{ re: /(adr|架构决策|技术决策|architecture decision)/i, domain: '2-system', category: 'decisions' },
|
|
43
|
+
{ re: /(服务概述|module overview|service overview|system overview)/i, domain: '2-system', category: 'services' },
|
|
44
|
+
{ re: /(runbook|排查|sop|on-call|playbook)/i, domain: '3-ops', category: 'runbooks' },
|
|
45
|
+
{ re: /(incident|事故|复盘|postmortem)/i, domain: '3-ops', category: 'incidents' },
|
|
46
|
+
{ re: /(环境|environment|infra|deploy)/i, domain: '3-ops', category: 'environments' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function classify(entry) {
|
|
50
|
+
const reasons = [];
|
|
51
|
+
if (entry.hints && entry.hints.domain && entry.hints.category) {
|
|
52
|
+
return {
|
|
53
|
+
domain: entry.hints.domain, category: entry.hints.category,
|
|
54
|
+
suggestedFilename: filename(entry, entry.hints),
|
|
55
|
+
confidence: 1.0,
|
|
56
|
+
reasons: ['explicit hint'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let pick = null;
|
|
61
|
+
// 1. Heading hints win first (most specific signal).
|
|
62
|
+
for (const r of SECTION_HINTS) {
|
|
63
|
+
if (entry.section && r.re.test(entry.section)) {
|
|
64
|
+
pick = { domain: r.domain, category: r.category };
|
|
65
|
+
reasons.push(`heading: ${r.re}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// 2. Artifact-based fallback.
|
|
70
|
+
if (!pick) {
|
|
71
|
+
for (const r of ARTIFACT_RULES) {
|
|
72
|
+
if (r.match.test(entry.artifact || '')) {
|
|
73
|
+
pick = { domain: r.domain, category: r.category };
|
|
74
|
+
reasons.push(`artifact: ${r.match}`);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 3. Body keyword fallback (broadest).
|
|
80
|
+
if (!pick) {
|
|
81
|
+
for (const r of SECTION_HINTS) {
|
|
82
|
+
if (r.re.test(entry.content || '')) {
|
|
83
|
+
pick = { domain: r.domain, category: r.category };
|
|
84
|
+
reasons.push(`content: ${r.re}`);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!pick) {
|
|
90
|
+
pick = { domain: '4-archive', category: 'solutions' };
|
|
91
|
+
reasons.push('default fallback');
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
domain: pick.domain, category: pick.category,
|
|
95
|
+
suggestedFilename: filename(entry, pick),
|
|
96
|
+
confidence: reasons.length === 1 ? 0.5 : 0.8,
|
|
97
|
+
reasons,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function filename(entry, pick) {
|
|
102
|
+
const sect = (entry.section || '').trim();
|
|
103
|
+
const slug = (entry.slug || 'note').replace(/[^a-zA-Z0-9-]+/g, '-');
|
|
104
|
+
let base;
|
|
105
|
+
if (sect) {
|
|
106
|
+
base = safe(sect);
|
|
107
|
+
} else {
|
|
108
|
+
base = safe(slug);
|
|
109
|
+
}
|
|
110
|
+
// Add slug suffix to avoid collisions across changes.
|
|
111
|
+
if (!base.includes(slug)) base = `${base}__${slug}`;
|
|
112
|
+
return `${base}.md`.slice(0, 200);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function safe(s) {
|
|
116
|
+
return s
|
|
117
|
+
.normalize('NFKC')
|
|
118
|
+
.replace(/[\\\/:*?"<>|\n\r\t]/g, '-')
|
|
119
|
+
.replace(/\s+/g, '-')
|
|
120
|
+
.replace(/-{2,}/g, '-')
|
|
121
|
+
.replace(/^-+|-+$/g, '')
|
|
122
|
+
.slice(0, 80);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { classify };
|