@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,285 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs/promises');
4
+ const fsSync = require('fs');
5
+ const path = require('path');
6
+ const change = require('./change.js');
7
+
8
+ const PROJECT_MARKERS = [
9
+ 'pom.xml',
10
+ 'build.gradle',
11
+ 'build.gradle.kts',
12
+ 'go.mod',
13
+ 'package.json',
14
+ path.join('devflow', 'config.json'),
15
+ '.git',
16
+ ];
17
+
18
+ const DEFAULT_IGNORE_DIRS = new Set([
19
+ '.git',
20
+ 'node_modules',
21
+ 'target',
22
+ 'build',
23
+ 'dist',
24
+ 'vendor',
25
+ '.idea',
26
+ '.vscode',
27
+ '.devflow-worktrees',
28
+ ]);
29
+
30
+ const TEXT_EXTENSIONS = new Set([
31
+ '.java', '.go', '.js', '.ts', '.tsx', '.jsx', '.vue',
32
+ '.xml', '.yml', '.yaml', '.json', '.md', '.properties',
33
+ '.sql', '.proto', '.gradle', '.kt', '.kts',
34
+ ]);
35
+
36
+ async function discoverProjects(searchRoots, opts = {}) {
37
+ const roots = normalizeRoots(searchRoots);
38
+ const maxDepth = Number.isFinite(opts.maxDepth) ? opts.maxDepth : 3;
39
+ const found = new Map();
40
+
41
+ for (const root of roots) {
42
+ await walkProjectRoots(root, 0, maxDepth, found);
43
+ }
44
+
45
+ return Array.from(found.values())
46
+ .sort((a, b) => a.root.localeCompare(b.root));
47
+ }
48
+
49
+ async function routeProjects({ searchRoots, query, maxDepth, topK = 5 } = {}) {
50
+ const projects = await discoverProjects(searchRoots, { maxDepth });
51
+ const ranked = await rankProjects(projects, query || '', { topK });
52
+ return {
53
+ searchRoots: normalizeRoots(searchRoots),
54
+ candidates: ranked,
55
+ recommended: ranked
56
+ .filter((p) => p.confidence > 0)
57
+ .slice(0, topK)
58
+ .map((p, i) => ({
59
+ name: p.name,
60
+ root: p.root,
61
+ role: i === 0 ? 'primary' : 'dependency',
62
+ branch: change.currentBranch(p.root) || null,
63
+ confidence: p.confidence,
64
+ evidence: p.evidence,
65
+ })),
66
+ };
67
+ }
68
+
69
+ function explicitProjectRouting(projectRoots, { source = 'cli' } = {}) {
70
+ const roots = normalizeRoots(projectRoots);
71
+ const recommended = roots.map((root, i) => {
72
+ const branch = change.currentBranch(root);
73
+ return {
74
+ name: path.basename(root),
75
+ root,
76
+ role: i === 0 ? 'primary' : 'dependency',
77
+ branch: branch || null,
78
+ confidence: 1,
79
+ evidence: ['用户已指定'],
80
+ };
81
+ });
82
+ return {
83
+ status: 'explicit',
84
+ source,
85
+ searchRoots: roots,
86
+ queryTerms: [],
87
+ candidates: recommended,
88
+ recommended,
89
+ slugBranchSource: recommended[0] ? recommended[0].root : null,
90
+ };
91
+ }
92
+
93
+ async function writeProjectRootsArtifact(root, slug, projectRouting, { source = 'project-routing' } = {}) {
94
+ if (!projectRouting || !Array.isArray(projectRouting.recommended)) return null;
95
+ const doc = {
96
+ version: 1,
97
+ source,
98
+ status: projectRouting.status || null,
99
+ updatedAt: new Date().toISOString(),
100
+ searchRoots: projectRouting.searchRoots || [],
101
+ projects: projectRouting.recommended.map((p) => ({
102
+ name: p.name,
103
+ root: p.root,
104
+ role: p.role || null,
105
+ branch: p.branch || null,
106
+ confidence: p.confidence,
107
+ evidence: p.evidence || [],
108
+ })),
109
+ candidates: (projectRouting.candidates || []).slice(0, 10).map((p) => ({
110
+ name: p.name,
111
+ root: p.root,
112
+ marker: p.marker || null,
113
+ confidence: p.confidence,
114
+ evidence: p.evidence || [],
115
+ })),
116
+ };
117
+ await change.writeArtifact(root, slug, 'project-roots.json', JSON.stringify(doc, null, 2) + '\n');
118
+ return doc;
119
+ }
120
+
121
+ async function rankProjects(projects, query, opts = {}) {
122
+ const terms = extractTerms(query);
123
+ const topK = Number.isFinite(opts.topK) ? opts.topK : projects.length;
124
+ const ranked = [];
125
+
126
+ for (const project of projects) {
127
+ const evidence = [];
128
+ let score = scoreName(project, terms, evidence);
129
+ const hits = await searchProject(project.root, terms, { maxFiles: opts.maxFiles || 300 });
130
+ for (const h of hits) {
131
+ score += h.score;
132
+ evidence.push(h.text);
133
+ }
134
+ const confidence = terms.length ? Math.min(0.99, +(score / Math.max(terms.length * 5, 1)).toFixed(2)) : 0;
135
+ ranked.push({ ...project, score, confidence, evidence: dedupe(evidence).slice(0, 8) });
136
+ }
137
+
138
+ return ranked
139
+ .sort((a, b) => b.score - a.score || a.root.localeCompare(b.root))
140
+ .slice(0, topK);
141
+ }
142
+
143
+ function extractTerms(input) {
144
+ const text = String(input || '');
145
+ const out = [];
146
+ const seen = new Set();
147
+ const add = (term) => {
148
+ const s = String(term || '').trim();
149
+ if (!s || s.length < 2) return;
150
+ const key = s.toLowerCase();
151
+ if (seen.has(key)) return;
152
+ seen.add(key);
153
+ out.push(s);
154
+ };
155
+
156
+ for (const m of text.matchAll(/\/[A-Za-z0-9_./{}:-]{2,}/g)) add(m[0]);
157
+ for (const m of text.matchAll(/[A-Za-z][A-Za-z0-9_.$-]{2,}/g)) add(m[0]);
158
+ for (const m of text.matchAll(/[\u4e00-\u9fa5]{2,}/g)) add(m[0]);
159
+ return out.slice(0, 40);
160
+ }
161
+
162
+ async function walkProjectRoots(dir, depth, maxDepth, found) {
163
+ if (!fsSync.existsSync(dir)) return;
164
+ const real = path.resolve(dir);
165
+ const marker = firstProjectMarker(real);
166
+ if (marker) {
167
+ found.set(real, { name: path.basename(real), root: real, marker });
168
+ if (marker === '.git' && depth === 0 && depth < maxDepth) {
169
+ // A multi-project workspace can be a shallow git checkout whose children
170
+ // are the real service projects. Keep walking so entry routing can still
171
+ // pick a service branch before creating the change.
172
+ } else {
173
+ return;
174
+ }
175
+ }
176
+ if (depth >= maxDepth) return;
177
+
178
+ let entries;
179
+ try {
180
+ entries = await fs.readdir(real, { withFileTypes: true });
181
+ } catch {
182
+ return;
183
+ }
184
+ for (const e of entries) {
185
+ if (!e.isDirectory()) continue;
186
+ if (DEFAULT_IGNORE_DIRS.has(e.name)) continue;
187
+ await walkProjectRoots(path.join(real, e.name), depth + 1, maxDepth, found);
188
+ }
189
+ }
190
+
191
+ function firstProjectMarker(dir) {
192
+ for (const marker of PROJECT_MARKERS) {
193
+ if (fsSync.existsSync(path.join(dir, marker))) return marker;
194
+ }
195
+ return null;
196
+ }
197
+
198
+ function scoreName(project, terms, evidence) {
199
+ let score = 0;
200
+ const haystack = `${project.name} ${project.root}`.toLowerCase();
201
+ for (const term of terms) {
202
+ if (haystack.includes(term.toLowerCase())) {
203
+ score += 2;
204
+ evidence.push(`project name/path matches "${term}"`);
205
+ }
206
+ }
207
+ return score;
208
+ }
209
+
210
+ async function searchProject(root, terms, opts = {}) {
211
+ if (!terms.length) return [];
212
+ const files = [];
213
+ await collectTextFiles(root, files, { maxFiles: opts.maxFiles || 300, maxDepth: opts.maxDepth || 8 });
214
+ const hits = [];
215
+ const lowerTerms = terms.map((t) => ({ raw: t, lower: t.toLowerCase() }));
216
+
217
+ for (const file of files) {
218
+ const rel = path.relative(root, file);
219
+ const relLower = rel.toLowerCase();
220
+ for (const term of lowerTerms) {
221
+ if (relLower.includes(term.lower)) {
222
+ hits.push({ score: 4, text: `file path matches "${term.raw}": ${rel}` });
223
+ }
224
+ }
225
+
226
+ let content;
227
+ try {
228
+ const stat = await fs.stat(file);
229
+ if (stat.size > 512 * 1024) continue;
230
+ content = await fs.readFile(file, 'utf8');
231
+ } catch {
232
+ continue;
233
+ }
234
+ const lower = content.toLowerCase();
235
+ for (const term of lowerTerms) {
236
+ const idx = lower.indexOf(term.lower);
237
+ if (idx !== -1) {
238
+ hits.push({ score: 3, text: `content matches "${term.raw}": ${rel}` });
239
+ }
240
+ }
241
+ }
242
+ return hits;
243
+ }
244
+
245
+ async function collectTextFiles(dir, out, opts, depth = 0) {
246
+ if (out.length >= opts.maxFiles || depth > opts.maxDepth) return;
247
+ let entries;
248
+ try {
249
+ entries = await fs.readdir(dir, { withFileTypes: true });
250
+ } catch {
251
+ return;
252
+ }
253
+ for (const e of entries) {
254
+ if (out.length >= opts.maxFiles) return;
255
+ if (e.isDirectory()) {
256
+ if (DEFAULT_IGNORE_DIRS.has(e.name)) continue;
257
+ await collectTextFiles(path.join(dir, e.name), out, opts, depth + 1);
258
+ continue;
259
+ }
260
+ if (!e.isFile()) continue;
261
+ const ext = path.extname(e.name);
262
+ if (TEXT_EXTENSIONS.has(ext) || e.name === 'pom.xml' || e.name === 'go.mod') {
263
+ out.push(path.join(dir, e.name));
264
+ }
265
+ }
266
+ }
267
+
268
+ function normalizeRoots(searchRoots) {
269
+ const list = Array.isArray(searchRoots) ? searchRoots : [searchRoots || process.cwd()];
270
+ return dedupe(list.filter(Boolean).map((p) => path.resolve(String(p))));
271
+ }
272
+
273
+ function dedupe(items) {
274
+ return Array.from(new Set(items));
275
+ }
276
+
277
+ module.exports = {
278
+ discoverProjects,
279
+ rankProjects,
280
+ routeProjects,
281
+ explicitProjectRouting,
282
+ writeProjectRootsArtifact,
283
+ extractTerms,
284
+ PROJECT_MARKERS,
285
+ };
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('./paths.js');
6
+ const userconfig = require('../providers/userconfig.js');
7
+ const lifecycle = require('../providers/lifecycle.js');
8
+
9
+ const DEFAULT_PROJECTS = {
10
+ version: 1,
11
+ environments: {},
12
+ projects: {},
13
+ defaults: {},
14
+ };
15
+
16
+ async function read() {
17
+ const file = paths.userProjectsFile();
18
+ if (!fsSync.existsSync(file)) {
19
+ const legacy = paths.legacyUserProjectsFile && paths.legacyUserProjectsFile();
20
+ if (legacy && fsSync.existsSync(legacy)) {
21
+ try {
22
+ const data = JSON.parse(await fs.readFile(legacy, 'utf8'));
23
+ return normalize(data);
24
+ } catch {
25
+ return clone(DEFAULT_PROJECTS);
26
+ }
27
+ }
28
+ return clone(DEFAULT_PROJECTS);
29
+ }
30
+ try {
31
+ const data = JSON.parse(await fs.readFile(file, 'utf8'));
32
+ return normalize(data);
33
+ } catch {
34
+ return clone(DEFAULT_PROJECTS);
35
+ }
36
+ }
37
+
38
+ async function write(cfg) {
39
+ await userconfig.ensureUserConfig();
40
+ const file = paths.userProjectsFile();
41
+ const data = normalize(cfg);
42
+ await fs.mkdir(path.dirname(file), { recursive: true });
43
+ await fs.writeFile(file, JSON.stringify(data, null, 2) + '\n', 'utf8');
44
+ await lifecycle.applySecureMode(file);
45
+ return file;
46
+ }
47
+
48
+ async function importPublishCodeConfig(file) {
49
+ const raw = JSON.parse(await fs.readFile(path.resolve(file), 'utf8'));
50
+ const cur = await read();
51
+ const out = normalize(cur);
52
+ const list = Array.isArray(raw.projectsList) ? raw.projectsList : [];
53
+ let imported = 0;
54
+ let skipped = 0;
55
+
56
+ for (const p of list) {
57
+ if (!p || !p.name) { skipped++; continue; }
58
+ out.projects[p.name] = {
59
+ ...(out.projects[p.name] || {}),
60
+ name: p.name,
61
+ id: p.id || undefined,
62
+ repoUrl: p.repoUrl || undefined,
63
+ gitlabProjectId: p.projectId ? String(p.projectId) : undefined,
64
+ tagFormat: p.tagFormat || undefined,
65
+ jenkins: compact({
66
+ test: p.testJenkinsUrl || undefined,
67
+ staging: p.stagingJenkinsUrl || undefined,
68
+ }),
69
+ deploy: mergeImportedDeployConfig(out.projects[p.name] && out.projects[p.name].deploy, p),
70
+ };
71
+ imported++;
72
+ }
73
+
74
+ if (raw.deployInfo && typeof raw.deployInfo === 'object') {
75
+ out.defaults.lastDeploy = compact({
76
+ project: raw.deployInfo.project_name,
77
+ currTag: raw.deployInfo.curr_tag_name,
78
+ oldTag: raw.deployInfo.old_tag_name,
79
+ modifyDesc: raw.deployInfo.modify_desc,
80
+ tester: raw.deployInfo.tester,
81
+ acceptor: raw.deployInfo.acceptor,
82
+ developerApprover: raw.deployInfo.developerApprover,
83
+ });
84
+ }
85
+
86
+ await write(out);
87
+ await seedProviderPlaceholders();
88
+ return { imported, skipped, file: paths.userProjectsFile() };
89
+ }
90
+
91
+ async function seedProviderPlaceholders() {
92
+ await userconfig.ensureUserConfig();
93
+ const file = paths.userProvidersFile();
94
+ let cur = {};
95
+ if (fsSync.existsSync(file)) {
96
+ try { cur = JSON.parse(await fs.readFile(file, 'utf8')); } catch { cur = {}; }
97
+ }
98
+ if (!cur['ci.jenkins']) {
99
+ cur['ci.jenkins'] = {
100
+ type: 'ci',
101
+ driver: 'jenkins',
102
+ config: {
103
+ user: '${env:JENKINS_USER}',
104
+ token: '${env:JENKINS_TOKEN}',
105
+ },
106
+ };
107
+ }
108
+ await fs.writeFile(file, JSON.stringify(cur, null, 2) + '\n', 'utf8');
109
+ await lifecycle.applySecureMode(file);
110
+ }
111
+
112
+ async function findProject(name) {
113
+ const cfg = await read();
114
+ const key = name || (cfg.defaults && cfg.defaults.lastDeploy && cfg.defaults.lastDeploy.project);
115
+ if (!key) return null;
116
+ return cfg.projects[key] || null;
117
+ }
118
+
119
+ async function resolveTestCommand({ project, env = 'test', kind, api = false }) {
120
+ const cfg = await read();
121
+ const p = project ? cfg.projects[project] : null;
122
+ const environment = cfg.environments && cfg.environments[env] ? cfg.environments[env] : {};
123
+ const tests = p && p.tests ? p.tests : {};
124
+ const profile = tests[kind] || {};
125
+ let command = profile.command;
126
+
127
+ if (!command && api) {
128
+ if (!environment.apiBaseUrl) {
129
+ return { command: null, reason: `apiBaseUrl missing for env=${env}` };
130
+ }
131
+ command = 'curl -fsS ${apiBaseUrl}/health';
132
+ }
133
+
134
+ if (!command) return { command: null, reason: `no ${kind} test command for project=${project || '-'}` };
135
+ return {
136
+ command: render(command, {
137
+ project,
138
+ env,
139
+ apiBaseUrl: environment.apiBaseUrl || '',
140
+ }),
141
+ framework: profile.framework || 'generic',
142
+ project,
143
+ env,
144
+ };
145
+ }
146
+
147
+ function normalize(data) {
148
+ return {
149
+ version: data.version || 1,
150
+ environments: data.environments || {},
151
+ projects: data.projects || {},
152
+ defaults: data.defaults || {},
153
+ };
154
+ }
155
+
156
+ function mergeImportedDeployConfig(existing, projectInput) {
157
+ const deploy = { ...(existing || {}) };
158
+ for (const [env, url] of [
159
+ ['test', projectInput.testJenkinsUrl],
160
+ ['staging', projectInput.stagingJenkinsUrl],
161
+ ]) {
162
+ if (!isUrl(url)) continue;
163
+ deploy[env] = {
164
+ ...(deploy[env] || {}),
165
+ includeDefaultParams: deploy[env] && deploy[env].includeDefaultParams !== undefined
166
+ ? deploy[env].includeDefaultParams
167
+ : false,
168
+ };
169
+ }
170
+ return deploy;
171
+ }
172
+
173
+ function render(s, vars) {
174
+ return String(s).replace(/\$\{([a-zA-Z0-9_]+)\}/g, (_, k) => vars[k] == null ? '' : String(vars[k]));
175
+ }
176
+
177
+ function compact(obj) {
178
+ const out = {};
179
+ for (const [k, v] of Object.entries(obj)) {
180
+ if (v !== undefined && v !== null && v !== '') out[k] = v;
181
+ }
182
+ return out;
183
+ }
184
+
185
+ function clone(v) {
186
+ return JSON.parse(JSON.stringify(v));
187
+ }
188
+
189
+ function isUrl(value) {
190
+ return /^https?:\/\//i.test(String(value || ''));
191
+ }
192
+
193
+ module.exports = {
194
+ read,
195
+ write,
196
+ importPublishCodeConfig,
197
+ findProject,
198
+ resolveTestCommand,
199
+ _internals: { normalize, render, compact, mergeImportedDeployConfig },
200
+ };
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const path = require('path');
4
+ const paths = require('./paths.js');
5
+ const fsSync = require('fs');
6
+
7
+ const STATE_VERSION = 1;
8
+
9
+ const PHASES = [
10
+ 'intake',
11
+ 'requirement',
12
+ 'design',
13
+ 'plan',
14
+ 'apply',
15
+ 'review',
16
+ 'verify',
17
+ 'deliver',
18
+ 'archive',
19
+ ];
20
+
21
+ const DEFAULT_ENABLED = {
22
+ 'tests.md': false,
23
+ 'delta': false,
24
+ 'reports.unit': false,
25
+ 'reports.integration': false,
26
+ 'reports.e2e': false,
27
+ 'reports.smoke': false,
28
+ 'reports.self_test': false,
29
+ 'reports.regression': false,
30
+ 'reports.perf': false,
31
+ };
32
+
33
+ function newState({ slug, title, level = 'L2', source }) {
34
+ const now = new Date().toISOString();
35
+ const phases = {};
36
+ for (const p of PHASES) phases[p] = { status: 'pending' };
37
+ return {
38
+ $schema: 'https://devflow.dev/schemas/state.schema.json',
39
+ version: STATE_VERSION,
40
+ slug,
41
+ title: title || slug,
42
+ level,
43
+ source: source || null,
44
+ currentPhase: 'intake',
45
+ phases,
46
+ checkpoints: [],
47
+ riskSignals: [],
48
+ iterations: { review: 0, apply: 0 },
49
+ enabled: applyLevelDefaults(level, { ...DEFAULT_ENABLED }),
50
+ providers: {},
51
+ auditLog: [{ ts: now, event: 'created' }],
52
+ createdAt: now,
53
+ updatedAt: now,
54
+ };
55
+ }
56
+
57
+ function applyLevelDefaults(level, enabled) {
58
+ if (level === 'L0') {
59
+ enabled['reports.unit'] = true;
60
+ } else if (level === 'L1') {
61
+ enabled['reports.unit'] = true;
62
+ enabled['reports.smoke'] = true;
63
+ } else if (level === 'L2') {
64
+ enabled['reports.unit'] = true;
65
+ enabled['reports.integration'] = true;
66
+ enabled['reports.smoke'] = true;
67
+ enabled['reports.self_test'] = true;
68
+ } else if (level === 'L3') {
69
+ for (const k of Object.keys(enabled)) enabled[k] = true;
70
+ enabled['reports.perf'] = false;
71
+ }
72
+ return enabled;
73
+ }
74
+
75
+ async function read(root, slug) {
76
+ const file = resolveStateFile(root, slug);
77
+ const buf = await fs.readFile(file, 'utf8');
78
+ return JSON.parse(buf);
79
+ }
80
+
81
+ async function write(root, slug, state) {
82
+ state.updatedAt = new Date().toISOString();
83
+ const file = resolveStateFile(root, slug, { forWrite: true });
84
+ await fs.mkdir(path.dirname(file), { recursive: true });
85
+ await fs.writeFile(file, JSON.stringify(state, null, 2) + '\n', 'utf8');
86
+ }
87
+
88
+ function resolveStateFile(root, slug, { forWrite = false } = {}) {
89
+ const workspace = paths.stateFile(root, slug);
90
+ const legacy = path.join(paths.devflowDir(root), 'changes', slug, 'state.json');
91
+ const legacyDir = path.dirname(legacy);
92
+ if (forWrite) {
93
+ if (fsSync.existsSync(workspace)) return workspace;
94
+ if (fsSync.existsSync(legacyDir)) return legacy;
95
+ return workspace;
96
+ }
97
+ if (fsSync.existsSync(workspace)) return workspace;
98
+ if (fsSync.existsSync(legacy)) return legacy;
99
+ return workspace;
100
+ }
101
+
102
+ function logEvent(state, event, extra) {
103
+ state.auditLog = state.auditLog || [];
104
+ state.auditLog.push({ ts: new Date().toISOString(), event, ...(extra || {}) });
105
+ }
106
+
107
+ function setPhase(state, phase, status) {
108
+ if (!state.phases[phase]) state.phases[phase] = {};
109
+ state.phases[phase].status = status;
110
+ state.phases[phase].ts = new Date().toISOString();
111
+ if (status === 'in_progress' || status === 'completed') {
112
+ state.currentPhase = phase;
113
+ }
114
+ }
115
+
116
+ function passCheckpoint(state, name) {
117
+ state.checkpoints = state.checkpoints || [];
118
+ state.checkpoints.push({
119
+ name,
120
+ status: 'passed',
121
+ ts: new Date().toISOString(),
122
+ });
123
+ }
124
+
125
+ function addRiskSignal(state, { type, reason, source = 'manual', evidence = [], status = 'open' }) {
126
+ if (!type) throw new Error('risk signal type is required');
127
+ state.riskSignals = state.riskSignals || [];
128
+ const now = new Date().toISOString();
129
+ const existing = state.riskSignals.find((r) => r.type === type && (r.status || 'open') === 'open');
130
+ if (existing) {
131
+ existing.reason = reason || existing.reason || '';
132
+ existing.source = source || existing.source || 'manual';
133
+ existing.evidence = mergeList(existing.evidence, evidence);
134
+ existing.updatedAt = now;
135
+ return existing;
136
+ }
137
+ const item = {
138
+ type,
139
+ status,
140
+ reason: reason || '',
141
+ source,
142
+ evidence: Array.isArray(evidence) ? evidence : [evidence].filter(Boolean),
143
+ createdAt: now,
144
+ updatedAt: now,
145
+ };
146
+ state.riskSignals.push(item);
147
+ return item;
148
+ }
149
+
150
+ function openRiskSignals(state, types) {
151
+ const allowed = types ? new Set(types) : null;
152
+ return (state.riskSignals || []).filter((r) => {
153
+ const status = r.status || 'open';
154
+ return status === 'open' && (!allowed || allowed.has(r.type));
155
+ });
156
+ }
157
+
158
+ function resolveRiskSignal(state, type, { evidence = [], reason = '' } = {}) {
159
+ return closeRiskSignal(state, type, 'resolved', { evidence, reason });
160
+ }
161
+
162
+ function acceptRiskSignal(state, type, { reason = '', evidence = [] } = {}) {
163
+ return closeRiskSignal(state, type, 'accepted', { evidence, reason });
164
+ }
165
+
166
+ function closeRiskSignal(state, type, status, { evidence = [], reason = '' } = {}) {
167
+ state.riskSignals = state.riskSignals || [];
168
+ const now = new Date().toISOString();
169
+ const targets = state.riskSignals.filter((r) => r.type === type && (r.status || 'open') === 'open');
170
+ for (const r of targets) {
171
+ r.status = status;
172
+ r.reason = reason || r.reason || '';
173
+ r.evidence = mergeList(r.evidence, evidence);
174
+ r.updatedAt = now;
175
+ }
176
+ return targets;
177
+ }
178
+
179
+ function mergeList(a = [], b = []) {
180
+ const left = Array.isArray(a) ? a : [a].filter(Boolean);
181
+ const right = Array.isArray(b) ? b : [b].filter(Boolean);
182
+ return [...new Set([...left, ...right].filter(Boolean))];
183
+ }
184
+
185
+ module.exports = {
186
+ STATE_VERSION,
187
+ PHASES,
188
+ DEFAULT_ENABLED,
189
+ newState,
190
+ applyLevelDefaults,
191
+ read,
192
+ write,
193
+ logEvent,
194
+ setPhase,
195
+ passCheckpoint,
196
+ addRiskSignal,
197
+ openRiskSignals,
198
+ resolveRiskSignal,
199
+ acceptRiskSignal,
200
+ };