@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,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 };
|