@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,271 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const cp = require('child_process');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Auto-detection helpers for `devflow init` — all functions return safe defaults
|
|
9
|
+
* when detection fails (never throw).
|
|
10
|
+
*
|
|
11
|
+
* Mirrors arb-workflow-kit/skills/dev-workflow/scripts/init-project.py but
|
|
12
|
+
* ships as a library rather than a generator script.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Detect git remote URL; returns null when not a git repo or no remote. */
|
|
16
|
+
function gitRemote(root, name = 'origin') {
|
|
17
|
+
try {
|
|
18
|
+
const r = cp.spawnSync('git', ['config', '--get', `remote.${name}.url`], {
|
|
19
|
+
cwd: root, encoding: 'utf8',
|
|
20
|
+
});
|
|
21
|
+
if (r.status !== 0) return null;
|
|
22
|
+
const url = (r.stdout || '').trim();
|
|
23
|
+
return url || null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse a git remote URL into { host, group, project }.
|
|
29
|
+
*
|
|
30
|
+
* Handles:
|
|
31
|
+
* - git@gitlab.corp.com:group/subgroup/project.git
|
|
32
|
+
* - ssh://git@gitlab.corp.com:2222/group/project.git
|
|
33
|
+
* - https://gitlab.corp.com/group/project
|
|
34
|
+
* - https://gitlab.corp.com/group/subgroup/project.git
|
|
35
|
+
*/
|
|
36
|
+
function parseRemote(url) {
|
|
37
|
+
if (!url) return null;
|
|
38
|
+
let host, pathPart;
|
|
39
|
+
|
|
40
|
+
// git@host:group/project.git
|
|
41
|
+
const sshShort = /^([\w.-]+@)?([\w.-]+):(.+?)(?:\.git)?\/?$/.exec(url);
|
|
42
|
+
// ssh://git@host[:port]/group/project(.git)?
|
|
43
|
+
const sshLong = /^ssh:\/\/(?:[\w.-]+@)?([\w.-]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/.exec(url);
|
|
44
|
+
// https?://host/group/project(.git)?
|
|
45
|
+
const https = /^https?:\/\/(?:[^@/]+@)?([\w.-]+)(?::\d+)?\/(.+?)(?:\.git)?\/?$/.exec(url);
|
|
46
|
+
|
|
47
|
+
if (sshLong) { host = sshLong[1]; pathPart = sshLong[2]; }
|
|
48
|
+
else if (https) { host = https[1]; pathPart = https[2]; }
|
|
49
|
+
else if (sshShort) { host = sshShort[2]; pathPart = sshShort[3]; }
|
|
50
|
+
else return null;
|
|
51
|
+
|
|
52
|
+
pathPart = pathPart.replace(/\.git$/, '').replace(/^\/+|\/+$/g, '');
|
|
53
|
+
const segs = pathPart.split('/').filter(Boolean);
|
|
54
|
+
if (segs.length < 2) return { host, group: '', project: segs[0] || '', fullPath: pathPart };
|
|
55
|
+
const project = segs.pop();
|
|
56
|
+
const group = segs.join('/');
|
|
57
|
+
return { host, group, project, fullPath: `${group}/${project}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detect the repo's default branch. Returns a best guess branch name
|
|
62
|
+
* (never throws). Priority:
|
|
63
|
+
*
|
|
64
|
+
* 1. `git symbolic-ref refs/remotes/origin/HEAD` — what the remote marks
|
|
65
|
+
* as HEAD (authoritative for cloned repos)
|
|
66
|
+
* 2. Existence of remote tracking `origin/master` → master
|
|
67
|
+
* 3. Existence of remote tracking `origin/main` → main
|
|
68
|
+
* 4. Existence of local branch `master` → master
|
|
69
|
+
* 5. Existence of local branch `main` → main
|
|
70
|
+
* 6. `git config init.defaultBranch` (when explicitly set)
|
|
71
|
+
* 7. Literal `'main'` (git 2.28+ default)
|
|
72
|
+
*
|
|
73
|
+
* We prefer `master` ahead of `main` when both remote/local flags must be
|
|
74
|
+
* inspected, because legacy repos (common in internal GitLab) still default
|
|
75
|
+
* to master; newer repos declare their HEAD via step 1.
|
|
76
|
+
*/
|
|
77
|
+
function detectDefaultBranch(root) {
|
|
78
|
+
const run = (args) => {
|
|
79
|
+
try { return cp.spawnSync('git', args, { cwd: root, encoding: 'utf8' }); }
|
|
80
|
+
catch { return { status: 1, stdout: '', stderr: '' }; }
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// 1. origin/HEAD symbolic ref — most reliable when present
|
|
84
|
+
let r = run(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
85
|
+
if (r.status === 0) {
|
|
86
|
+
const full = (r.stdout || '').trim(); // e.g. "origin/master"
|
|
87
|
+
if (full) {
|
|
88
|
+
const branch = full.replace(/^origin\//, '');
|
|
89
|
+
if (branch) return branch;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2-3. Remote tracking branches
|
|
94
|
+
for (const name of ['master', 'main']) {
|
|
95
|
+
r = run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${name}`]);
|
|
96
|
+
if (r.status === 0) return name;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4-5. Local branches (e.g. fresh repo, no remote yet)
|
|
100
|
+
for (const name of ['master', 'main']) {
|
|
101
|
+
r = run(['rev-parse', '--verify', '--quiet', `refs/heads/${name}`]);
|
|
102
|
+
if (r.status === 0) return name;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 6. Respect explicit git config
|
|
106
|
+
r = run(['config', '--get', 'init.defaultBranch']);
|
|
107
|
+
if (r.status === 0) {
|
|
108
|
+
const v = (r.stdout || '').trim();
|
|
109
|
+
if (v) return v;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 7. Sensible fallback (matches modern git default)
|
|
113
|
+
return 'main';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Detect primary language / stack from lock + manifest files.
|
|
118
|
+
* Returns first match (priority ordered) or 'unknown'.
|
|
119
|
+
*/
|
|
120
|
+
function detectLanguage(root) {
|
|
121
|
+
const checks = [
|
|
122
|
+
{ f: 'go.mod', lang: 'go' },
|
|
123
|
+
{ f: 'package.json', lang: 'node' },
|
|
124
|
+
{ f: 'pyproject.toml', lang: 'python' },
|
|
125
|
+
{ f: 'requirements.txt', lang: 'python' },
|
|
126
|
+
{ f: 'setup.cfg', lang: 'python' },
|
|
127
|
+
{ f: 'Cargo.toml', lang: 'rust' },
|
|
128
|
+
{ f: 'pom.xml', lang: 'java' },
|
|
129
|
+
{ f: 'build.gradle', lang: 'java' },
|
|
130
|
+
{ f: 'build.gradle.kts', lang: 'kotlin' },
|
|
131
|
+
{ f: 'composer.json', lang: 'php' },
|
|
132
|
+
{ f: 'Gemfile', lang: 'ruby' },
|
|
133
|
+
{ f: 'mix.exs', lang: 'elixir' },
|
|
134
|
+
{ f: '*.csproj', lang: 'dotnet', glob: true },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
for (const c of checks) {
|
|
138
|
+
if (c.glob) {
|
|
139
|
+
try {
|
|
140
|
+
const files = fs.readdirSync(root);
|
|
141
|
+
if (files.some((n) => n.endsWith('.csproj'))) return c.lang;
|
|
142
|
+
} catch { /* ignore */ }
|
|
143
|
+
} else if (fs.existsSync(path.join(root, c.f))) {
|
|
144
|
+
return c.lang;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Detect package manager / build tool for the current project.
|
|
152
|
+
*/
|
|
153
|
+
function detectBuildTool(root, lang) {
|
|
154
|
+
const exists = (f) => fs.existsSync(path.join(root, f));
|
|
155
|
+
if (lang === 'node') {
|
|
156
|
+
if (exists('pnpm-lock.yaml')) return 'pnpm';
|
|
157
|
+
if (exists('yarn.lock')) return 'yarn';
|
|
158
|
+
if (exists('bun.lockb')) return 'bun';
|
|
159
|
+
return 'npm';
|
|
160
|
+
}
|
|
161
|
+
if (lang === 'python') {
|
|
162
|
+
if (exists('poetry.lock')) return 'poetry';
|
|
163
|
+
if (exists('uv.lock')) return 'uv';
|
|
164
|
+
if (exists('Pipfile.lock')) return 'pipenv';
|
|
165
|
+
return 'pip';
|
|
166
|
+
}
|
|
167
|
+
if (lang === 'java') return exists('pom.xml') ? 'maven' : 'gradle';
|
|
168
|
+
if (lang === 'go') return 'go';
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Suggest a Jenkins job name based on project name + common conventions.
|
|
174
|
+
* Returns a list of candidates so UI can offer choices.
|
|
175
|
+
*
|
|
176
|
+
* - <project>-test
|
|
177
|
+
* - <project>-test2
|
|
178
|
+
* - <group>-<project>-test
|
|
179
|
+
* - deploy/<project>
|
|
180
|
+
*/
|
|
181
|
+
function suggestJenkinsJobs({ group, project, env = 'test' }) {
|
|
182
|
+
if (!project) return [];
|
|
183
|
+
const proj = project.replace(/[^\w-]/g, '-');
|
|
184
|
+
const grp = (group || '').replace(/[^\w-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
185
|
+
const envName = String(env || 'test').replace(/[^\w-]/g, '-');
|
|
186
|
+
const out = [
|
|
187
|
+
`${proj}-${envName}`,
|
|
188
|
+
`${proj}-test`,
|
|
189
|
+
`${proj}-test2`,
|
|
190
|
+
`${proj}-deploy-${envName}`,
|
|
191
|
+
`deploy/${proj}`,
|
|
192
|
+
`deploy/${proj}-${envName}`,
|
|
193
|
+
`${envName}/${proj}`,
|
|
194
|
+
];
|
|
195
|
+
if (grp) out.unshift(`${grp}-${proj}`, `${grp}-${proj}-${envName}`, `${grp}-${envName}-${proj}`);
|
|
196
|
+
return [...new Set(out)];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function rankJenkinsJobs(candidates, { group, project, env = 'test' } = {}) {
|
|
200
|
+
const expected = suggestJenkinsJobs({ group, project, env });
|
|
201
|
+
const scored = (candidates || []).map((job) => {
|
|
202
|
+
const name = String(job && (job.name || job.fullName || job.url || job) || '');
|
|
203
|
+
const normalized = name.toLowerCase();
|
|
204
|
+
let score = 0;
|
|
205
|
+
const exactIdx = expected.findIndex((x) => x.toLowerCase() === normalized);
|
|
206
|
+
if (exactIdx >= 0) score += 100 - exactIdx;
|
|
207
|
+
if (project && normalized.includes(String(project).toLowerCase())) score += 30;
|
|
208
|
+
if (group && normalized.includes(String(group).split('/').pop().toLowerCase())) score += 10;
|
|
209
|
+
if (env && normalized.includes(String(env).toLowerCase())) score += 20;
|
|
210
|
+
if (/deploy|发布|部署/.test(normalized)) score += 5;
|
|
211
|
+
if (/rollback|回滚/.test(normalized)) score -= 20;
|
|
212
|
+
return { job, name, score };
|
|
213
|
+
}).filter((x) => x.score > 0);
|
|
214
|
+
return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).map((x) => x.job);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Full one-shot detect used by `devflow init`.
|
|
219
|
+
* Returns object with detection flags + never-throws.
|
|
220
|
+
*/
|
|
221
|
+
async function detect(root) {
|
|
222
|
+
const remoteUrl = gitRemote(root);
|
|
223
|
+
const remote = parseRemote(remoteUrl);
|
|
224
|
+
const language = detectLanguage(root);
|
|
225
|
+
const buildTool = detectBuildTool(root, language);
|
|
226
|
+
const jenkinsJobs = remote ? suggestJenkinsJobs(remote) : [];
|
|
227
|
+
const defaultBranch = detectDefaultBranch(root);
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
remoteUrl,
|
|
231
|
+
remote, // { host, group, project, fullPath } or null
|
|
232
|
+
language, // 'go' | 'node' | 'python' | ...
|
|
233
|
+
buildTool,
|
|
234
|
+
jenkinsJobs,
|
|
235
|
+
defaultBranch, // 'main' | 'master' | <custom>
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Render detection result to human-readable summary for CLI output.
|
|
241
|
+
*/
|
|
242
|
+
function formatSummary(d) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
if (d.remote) {
|
|
245
|
+
lines.push(` remote: ${d.remote.host}:${d.remote.fullPath}`);
|
|
246
|
+
} else if (d.remoteUrl) {
|
|
247
|
+
lines.push(` remote: ${d.remoteUrl} (parse failed)`);
|
|
248
|
+
} else {
|
|
249
|
+
lines.push(` remote: (not a git repo or no origin)`);
|
|
250
|
+
}
|
|
251
|
+
lines.push(` language: ${d.language}${d.buildTool ? ` (${d.buildTool})` : ''}`);
|
|
252
|
+
if (d.defaultBranch) {
|
|
253
|
+
lines.push(` branch: ${d.defaultBranch} (default)`);
|
|
254
|
+
}
|
|
255
|
+
if (d.jenkinsJobs.length) {
|
|
256
|
+
lines.push(` jenkins: ${d.jenkinsJobs.slice(0, 3).join(' / ')}`);
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
gitRemote,
|
|
263
|
+
parseRemote,
|
|
264
|
+
detectLanguage,
|
|
265
|
+
detectBuildTool,
|
|
266
|
+
detectDefaultBranch,
|
|
267
|
+
suggestJenkinsJobs,
|
|
268
|
+
rankJenkinsJobs,
|
|
269
|
+
detect,
|
|
270
|
+
formatSummary,
|
|
271
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
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 state = require('./state.js');
|
|
8
|
+
|
|
9
|
+
function slugify(input) {
|
|
10
|
+
if (!input) return 'untitled-' + Date.now().toString(36);
|
|
11
|
+
return String(input)
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
14
|
+
.replace(/^-+|-+$/g, '')
|
|
15
|
+
.slice(0, 80);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function currentBranch(root) {
|
|
19
|
+
for (const args of [
|
|
20
|
+
['branch', '--show-current'],
|
|
21
|
+
['rev-parse', '--abbrev-ref', 'HEAD'],
|
|
22
|
+
]) {
|
|
23
|
+
const r = cp.spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
24
|
+
if (r.status !== 0) continue;
|
|
25
|
+
const branch = (r.stdout || '').trim();
|
|
26
|
+
if (branch && branch !== 'HEAD') return branch;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function withCurrentBranchSlug(root, slug) {
|
|
32
|
+
const safeSlug = slugify(slug);
|
|
33
|
+
const branch = currentBranch(root);
|
|
34
|
+
if (!branch) return safeSlug;
|
|
35
|
+
const branchSlug = slugify(branch);
|
|
36
|
+
if (!branchSlug) return safeSlug;
|
|
37
|
+
if (safeSlug === branchSlug || safeSlug.startsWith(`${branchSlug}-`)) return safeSlug;
|
|
38
|
+
return slugify(`${branchSlug}-${safeSlug}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function exists(p) {
|
|
42
|
+
try { await fs.access(p); return true; } catch { return false; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function listChanges(root) {
|
|
46
|
+
const dirs = [paths.changesDir(root), legacyChangesDir(root)];
|
|
47
|
+
const names = new Set();
|
|
48
|
+
for (const dir of dirs) {
|
|
49
|
+
if (!fsSync.existsSync(dir)) continue;
|
|
50
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
51
|
+
for (const e of entries) {
|
|
52
|
+
if (e.isDirectory()) names.add(e.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Array.from(names);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getCurrent(root) {
|
|
59
|
+
const all = await listChanges(root);
|
|
60
|
+
if (all.length === 0) {
|
|
61
|
+
// Fallback: look for the most recently archived change
|
|
62
|
+
return getLastArchived(root);
|
|
63
|
+
}
|
|
64
|
+
if (all.length === 1) return all[0];
|
|
65
|
+
// multi-change: pick most recently updated state.json
|
|
66
|
+
let best = null;
|
|
67
|
+
let bestTime = 0;
|
|
68
|
+
for (const slug of all) {
|
|
69
|
+
const file = resolveStateFile(root, slug);
|
|
70
|
+
if (!fsSync.existsSync(file)) continue;
|
|
71
|
+
const stat = await fs.stat(file);
|
|
72
|
+
if (stat.mtimeMs > bestTime) {
|
|
73
|
+
bestTime = stat.mtimeMs;
|
|
74
|
+
best = slug;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Return the slug of the most recently archived change (by dir mtime).
|
|
82
|
+
* Archive dirs are named `<YYYY-MM-DD>-<slug>`.
|
|
83
|
+
*/
|
|
84
|
+
async function getLastArchived(root) {
|
|
85
|
+
const all = [];
|
|
86
|
+
for (const dir of [paths.archiveDir(root), legacyArchiveDir(root)]) {
|
|
87
|
+
if (!fsSync.existsSync(dir)) continue;
|
|
88
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
if (e.isDirectory()) all.push({ base: dir, name: e.name });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const dirs = all;
|
|
94
|
+
if (!dirs.length) return null;
|
|
95
|
+
let best = null;
|
|
96
|
+
let bestTime = 0;
|
|
97
|
+
for (const e of dirs) {
|
|
98
|
+
const abs = path.join(e.base, e.name);
|
|
99
|
+
try {
|
|
100
|
+
const stat = await fs.stat(abs);
|
|
101
|
+
if (stat.mtimeMs > bestTime) {
|
|
102
|
+
bestTime = stat.mtimeMs;
|
|
103
|
+
best = e.name;
|
|
104
|
+
}
|
|
105
|
+
} catch { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
if (!best) return null;
|
|
108
|
+
// Strip leading date prefix: "2026-05-07-<slug>" → "<slug>"
|
|
109
|
+
const m = best.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
|
110
|
+
return m ? m[1] : best;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function create(root, opts) {
|
|
114
|
+
const slug = opts.slug;
|
|
115
|
+
if (!slug) throw new Error('change.create requires slug');
|
|
116
|
+
const dir = paths.changeDir(root, slug);
|
|
117
|
+
if (await exists(dir)) {
|
|
118
|
+
throw new Error(`change already exists: ${slug}`);
|
|
119
|
+
}
|
|
120
|
+
await fs.mkdir(dir, { recursive: true });
|
|
121
|
+
const st = state.newState(opts);
|
|
122
|
+
await state.write(root, slug, st);
|
|
123
|
+
return { slug, dir, state: st };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function ensureSubdir(root, slug, sub) {
|
|
127
|
+
const dir = await ensureWorkspaceChangeDir(root, slug);
|
|
128
|
+
const p = path.join(dir, sub);
|
|
129
|
+
await fs.mkdir(p, { recursive: true });
|
|
130
|
+
return p;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function writeArtifact(root, slug, name, content) {
|
|
134
|
+
const dir = await ensureWorkspaceChangeDir(root, slug);
|
|
135
|
+
const file = path.join(dir, name);
|
|
136
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
137
|
+
await fs.writeFile(file, content, 'utf8');
|
|
138
|
+
return file;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function readArtifact(root, slug, name) {
|
|
142
|
+
const workspaceFile = path.join(paths.changeDir(root, slug), name);
|
|
143
|
+
if (fsSync.existsSync(workspaceFile)) return fs.readFile(workspaceFile, 'utf8');
|
|
144
|
+
const legacyFile = path.join(legacyChangeDir(root, slug), name);
|
|
145
|
+
return fs.readFile(legacyFile, 'utf8');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function hasArtifact(root, slug, name) {
|
|
149
|
+
if (await exists(path.join(paths.changeDir(root, slug), name))) return true;
|
|
150
|
+
return exists(path.join(legacyChangeDir(root, slug), name));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function legacyChangesDir(root) {
|
|
154
|
+
return path.join(paths.devflowDir(root), 'changes');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function legacyArchiveDir(root) {
|
|
158
|
+
return path.join(paths.devflowDir(root), 'archive');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function legacyChangeDir(root, slug) {
|
|
162
|
+
return path.join(legacyChangesDir(root), slug);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function ensureWorkspaceChangeDir(root, slug) {
|
|
166
|
+
const workspace = paths.changeDir(root, slug);
|
|
167
|
+
const legacy = legacyChangeDir(root, slug);
|
|
168
|
+
if (fsSync.existsSync(workspace)) return workspace;
|
|
169
|
+
if (fsSync.existsSync(legacy)) {
|
|
170
|
+
try {
|
|
171
|
+
await fs.mkdir(path.dirname(workspace), { recursive: true });
|
|
172
|
+
await fs.cp(legacy, workspace, { recursive: true });
|
|
173
|
+
return workspace;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
if (e && (e.code === 'EACCES' || e.code === 'EPERM')) return legacy;
|
|
176
|
+
throw e;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
180
|
+
return workspace;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveChangeDir(root, slug) {
|
|
184
|
+
const workspace = paths.changeDir(root, slug);
|
|
185
|
+
const legacy = legacyChangeDir(root, slug);
|
|
186
|
+
if (fsSync.existsSync(workspace)) return workspace;
|
|
187
|
+
if (fsSync.existsSync(legacy)) return legacy;
|
|
188
|
+
return workspace;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveStateFile(root, slug) {
|
|
192
|
+
return path.join(resolveChangeDir(root, slug), 'state.json');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = {
|
|
196
|
+
slugify,
|
|
197
|
+
currentBranch,
|
|
198
|
+
withCurrentBranchSlug,
|
|
199
|
+
listChanges,
|
|
200
|
+
getCurrent,
|
|
201
|
+
create,
|
|
202
|
+
ensureSubdir,
|
|
203
|
+
writeArtifact,
|
|
204
|
+
readArtifact,
|
|
205
|
+
hasArtifact,
|
|
206
|
+
ensureWorkspaceChangeDir,
|
|
207
|
+
resolveChangeDir,
|
|
208
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
const VALID_STATUS = new Set(['pending', 'resolved', 'cancelled']);
|
|
6
|
+
|
|
7
|
+
function createCheckpoint(input = {}) {
|
|
8
|
+
const now = input.createdAt || new Date().toISOString();
|
|
9
|
+
const type = required(input.type, 'type');
|
|
10
|
+
const phase = input.phase || null;
|
|
11
|
+
const options = normalizeOptions(input.options || []);
|
|
12
|
+
return {
|
|
13
|
+
id: input.id || makeId(type, now),
|
|
14
|
+
type,
|
|
15
|
+
phase,
|
|
16
|
+
status: input.status || 'pending',
|
|
17
|
+
summary: input.summary || '',
|
|
18
|
+
question: input.question || '',
|
|
19
|
+
options,
|
|
20
|
+
defaultOption: input.defaultOption || (options[0] && options[0].id) || null,
|
|
21
|
+
nextAction: input.nextAction || (options[0] && options[0].command) || null,
|
|
22
|
+
risks: Array.isArray(input.risks) ? input.risks : [],
|
|
23
|
+
evidence: Array.isArray(input.evidence) ? input.evidence : [],
|
|
24
|
+
createdAt: now,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function addCheckpoint(state, checkpointInput) {
|
|
29
|
+
const checkpoint = createCheckpoint(checkpointInput);
|
|
30
|
+
state.checkpoints = Array.isArray(state.checkpoints) ? state.checkpoints : [];
|
|
31
|
+
state.checkpoints.push(checkpoint);
|
|
32
|
+
return checkpoint;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveCheckpoint(state, id, decision, note) {
|
|
36
|
+
const checkpoint = findCheckpoint(state, id);
|
|
37
|
+
if (!checkpoint) throw new Error(`checkpoint not found: ${id}`);
|
|
38
|
+
checkpoint.status = 'resolved';
|
|
39
|
+
checkpoint.decision = decision || null;
|
|
40
|
+
checkpoint.note = note || null;
|
|
41
|
+
checkpoint.resolvedAt = new Date().toISOString();
|
|
42
|
+
return checkpoint;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cancelCheckpoint(state, id, reason) {
|
|
46
|
+
const checkpoint = findCheckpoint(state, id);
|
|
47
|
+
if (!checkpoint) throw new Error(`checkpoint not found: ${id}`);
|
|
48
|
+
checkpoint.status = 'cancelled';
|
|
49
|
+
checkpoint.reason = reason || null;
|
|
50
|
+
checkpoint.resolvedAt = new Date().toISOString();
|
|
51
|
+
return checkpoint;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function latestPending(state) {
|
|
55
|
+
const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
|
|
56
|
+
for (let i = checkpoints.length - 1; i >= 0; i--) {
|
|
57
|
+
if (normalizeStatus(checkpoints[i].status) === 'pending') return checkpoints[i];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function latestPendingByType(state, type) {
|
|
63
|
+
const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
|
|
64
|
+
for (let i = checkpoints.length - 1; i >= 0; i--) {
|
|
65
|
+
const cp = checkpoints[i];
|
|
66
|
+
if (cp.type === type && normalizeStatus(cp.status) === 'pending') return cp;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function latestPendingByTypes(state, types) {
|
|
72
|
+
const wanted = new Set(types || []);
|
|
73
|
+
const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
|
|
74
|
+
for (let i = checkpoints.length - 1; i >= 0; i--) {
|
|
75
|
+
const cp = checkpoints[i];
|
|
76
|
+
if (wanted.has(cp.type) && normalizeStatus(cp.status) === 'pending') return cp;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasResolvedDecision(state, type, decision) {
|
|
82
|
+
const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
|
|
83
|
+
return checkpoints.some((cp) => {
|
|
84
|
+
if (cp.type !== type) return false;
|
|
85
|
+
if (normalizeStatus(cp.status) !== 'resolved') return false;
|
|
86
|
+
return decision ? cp.decision === decision : true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderNextStepCard(checkpoint, result = 'NEEDS_INPUT') {
|
|
91
|
+
if (!checkpoint) return '';
|
|
92
|
+
const options = normalizeOptions(checkpoint.options || []);
|
|
93
|
+
const lines = [
|
|
94
|
+
'## 下一步',
|
|
95
|
+
`状态:${result}`,
|
|
96
|
+
`检查点:${checkpoint.type}`,
|
|
97
|
+
];
|
|
98
|
+
if (checkpoint.phase) lines.push(`阶段:${checkpoint.phase}`);
|
|
99
|
+
if (checkpoint.summary) lines.push(`摘要:${checkpoint.summary}`);
|
|
100
|
+
if (checkpoint.question) lines.push(`需要你确认:${checkpoint.question}`);
|
|
101
|
+
if (options.length) {
|
|
102
|
+
lines.push('选项:');
|
|
103
|
+
for (const opt of options) {
|
|
104
|
+
const pieces = [`- ${opt.id}`];
|
|
105
|
+
if (opt.label && opt.label !== opt.id) pieces.push(opt.label);
|
|
106
|
+
if (opt.command) pieces.push(`命令:\`${opt.command}\``);
|
|
107
|
+
if (opt.description) pieces.push(opt.description);
|
|
108
|
+
lines.push(pieces.join(' — '));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const command = checkpoint.nextAction || (options[0] && options[0].command) || '<等待用户选择>';
|
|
112
|
+
lines.push(`命令:\`${command}\``);
|
|
113
|
+
if (checkpoint.risks && checkpoint.risks.length) lines.push(`风险:${checkpoint.risks.join(';')}`);
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildConfirmationCard(checkpoint, options = {}) {
|
|
118
|
+
if (!checkpoint) return null;
|
|
119
|
+
const normalized = normalizeOptions(checkpoint.options || []);
|
|
120
|
+
const primaryId = options.primaryActionId || checkpoint.defaultOption || (normalized[0] && normalized[0].id);
|
|
121
|
+
const primary = normalized.find((opt) => opt.id === primaryId) || normalized[0] || null;
|
|
122
|
+
return {
|
|
123
|
+
type: options.type || `${checkpoint.type}_confirm`,
|
|
124
|
+
title: options.title || '确认下一步',
|
|
125
|
+
question: checkpoint.question || '',
|
|
126
|
+
checkpoint: summarizeCheckpoint(checkpoint),
|
|
127
|
+
risks: Array.isArray(checkpoint.risks) ? checkpoint.risks.slice() : [],
|
|
128
|
+
evidence: Array.isArray(checkpoint.evidence) ? checkpoint.evidence.slice() : [],
|
|
129
|
+
primaryAction: primary ? actionFromOption(primary) : null,
|
|
130
|
+
secondaryActions: normalized
|
|
131
|
+
.filter((opt) => !primary || opt.id !== primary.id)
|
|
132
|
+
.map(actionFromOption),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function summarizeCheckpoint(checkpoint) {
|
|
137
|
+
return {
|
|
138
|
+
id: checkpoint.id,
|
|
139
|
+
type: checkpoint.type,
|
|
140
|
+
phase: checkpoint.phase || null,
|
|
141
|
+
status: checkpoint.status || 'pending',
|
|
142
|
+
summary: checkpoint.summary || '',
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function actionFromOption(option) {
|
|
147
|
+
return {
|
|
148
|
+
id: option.id,
|
|
149
|
+
label: option.label || option.id,
|
|
150
|
+
command: option.command || null,
|
|
151
|
+
description: option.description || '',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function parseOptions(value) {
|
|
156
|
+
if (!value) return [];
|
|
157
|
+
if (Array.isArray(value)) return normalizeOptions(value);
|
|
158
|
+
return String(value)
|
|
159
|
+
.split(';')
|
|
160
|
+
.map((entry) => entry.trim())
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map((entry) => {
|
|
163
|
+
const [left, ...rest] = entry.split('=');
|
|
164
|
+
const id = left.trim();
|
|
165
|
+
const command = rest.join('=').trim();
|
|
166
|
+
return { id, label: id, command };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeOptions(options) {
|
|
171
|
+
return options.map((opt) => {
|
|
172
|
+
if (typeof opt === 'string') return { id: opt, label: opt, command: null, description: '' };
|
|
173
|
+
const id = required(opt.id || opt.label, 'option.id');
|
|
174
|
+
return {
|
|
175
|
+
id,
|
|
176
|
+
label: opt.label || id,
|
|
177
|
+
command: opt.command || null,
|
|
178
|
+
description: opt.description || '',
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function findCheckpoint(state, id) {
|
|
184
|
+
const checkpoints = Array.isArray(state && state.checkpoints) ? state.checkpoints : [];
|
|
185
|
+
return checkpoints.find((c) => c.id === id || c.name === id) || null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizeStatus(status) {
|
|
189
|
+
const s = status || 'pending';
|
|
190
|
+
return VALID_STATUS.has(s) ? s : s === 'passed' ? 'resolved' : s;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function makeId(type, ts) {
|
|
194
|
+
const nonce = crypto.randomBytes(4).toString('hex');
|
|
195
|
+
const hash = crypto.createHash('sha1').update(`${type}:${ts}:${nonce}`).digest('hex').slice(0, 8);
|
|
196
|
+
return `${type}-${hash}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function required(value, name) {
|
|
200
|
+
if (!value) throw new Error(`checkpoint ${name} is required`);
|
|
201
|
+
return String(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
createCheckpoint,
|
|
206
|
+
addCheckpoint,
|
|
207
|
+
resolveCheckpoint,
|
|
208
|
+
cancelCheckpoint,
|
|
209
|
+
latestPending,
|
|
210
|
+
latestPendingByType,
|
|
211
|
+
latestPendingByTypes,
|
|
212
|
+
hasResolvedDecision,
|
|
213
|
+
renderNextStepCard,
|
|
214
|
+
buildConfirmationCard,
|
|
215
|
+
summarizeCheckpoint,
|
|
216
|
+
parseOptions,
|
|
217
|
+
};
|