@rowansenne/workflow-run 0.25.0 → 0.27.0
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 +38 -1
- package/README.md +60 -38
- package/bin/lib/artifact-context.mjs +46 -0
- package/bin/lib/core.mjs +5 -1
- package/bin/lib/docnexus.mjs +6 -6
- package/bin/lib/doctor.mjs +1 -2
- package/bin/lib/install.mjs +1 -1
- package/bin/lib/uninstall.mjs +3 -0
- package/bin/workflow-run.mjs +20 -29
- package/code-graph/SKILL.md.tpl +2 -7
- package/create-release-md-doc/SKILL.md.tpl +9 -8
- package/create-tech-md-doc/SKILL.md.tpl +9 -8
- package/database-query/SKILL.md.tpl +2 -0
- package/database-query/skill.json +2 -2
- package/dev/SKILL.md.tpl +42 -116
- package/dev/references/bug.md +30 -0
- package/dev/references/delivery-template.md +6 -6
- package/dev/references/delivery.md +48 -0
- package/dev/references/implementation.md +19 -0
- package/dev/references/plan-template.md +5 -3
- package/dev/references/planning.md +45 -0
- package/dev/references/preflight.md +35 -0
- package/dev/skill.json +2 -2
- package/md-to-wiki/SKILL.md.tpl +5 -1
- package/md-to-wiki/skill.json +2 -2
- package/package.json +2 -2
- package/project-review/SKILL.md.tpl +46 -72
- package/project-review/references/report.md.tpl +4 -4
- package/project-review/references/reporting.md +48 -0
- package/project-review/references/review-method.md +56 -0
- package/project-review/references/verification.md +30 -0
- package/shared/prd-fetch.md +1 -1
- package/workflow/SKILL.md.tpl +15 -16
- package/workflow/lib/artifact-model.mjs +83 -0
- package/workflow/lib/capabilities.mjs +80 -0
- package/workflow/lib/document-model.mjs +86 -0
- package/workflow/lib/meta-store.mjs +112 -0
- package/workflow/lib/project.mjs +218 -0
- package/workflow/lib/requirement-read.mjs +68 -0
- package/workflow/lib/requirements.mjs +219 -0
- package/workflow/lib/runs.mjs +144 -0
- package/workflow/lib/workspace-read.mjs +120 -0
- package/workflow/references/architecture.md +43 -0
- package/workflow/references/capabilities.md +39 -0
- package/workflow/references/codegraph.md +3 -3
- package/workflow/references/docnexus.md +1 -1
- package/workflow/references/requirements.md +25 -13
- package/workflow/references/runs.md +98 -0
- package/workflow/references/status.md +6 -3
- package/workflow/references/templates.md +11 -14
- package/workflow/scripts/artifacts.mjs +21 -80
- package/workflow/scripts/doc-sections.mjs +2 -55
- package/workflow/scripts/docnexus-layout.mjs +2 -24
- package/workflow/scripts/knowledge.mjs +4 -2
- package/workflow/scripts/lint-doc.mjs +2 -35
- package/workflow/scripts/meta-store.mjs +2 -0
- package/workflow/scripts/pr-desc.mjs +8 -6
- package/workflow/scripts/report.mjs +20 -68
- package/workflow/scripts/requirement.mjs +9 -402
- package/workflow/scripts/resolve.mjs +22 -190
- package/workflow/scripts/review-scope.mjs +2 -1
- package/workflow/scripts/run.mjs +37 -0
- package/workflow/scripts/tables.mjs +1 -1
- package/workflow/scripts/template.mjs +2 -6
- package/workflow/scripts/view.mjs +41 -21
|
@@ -11,21 +11,18 @@
|
|
|
11
11
|
// node .agents/skills/workflow/scripts/resolve.mjs status # 能力探测(/workflow status)
|
|
12
12
|
// node .agents/skills/workflow/scripts/resolve.mjs env # 本机系统与终端(安装时记录),写终端命令按它来
|
|
13
13
|
// node .agents/skills/workflow/scripts/resolve.mjs env --check # 核对画像里的开发环境配置(JDK / Maven / Node 路径)在本机是否存在
|
|
14
|
-
// 输出 JSON。退出码:0 正常;2
|
|
14
|
+
// 输出 JSON。退出码:0 正常;2 缺模板或必装 DocNexus 不可用(按共享能力与模板规则处理);1 参数错误 / 该 skill 没有声明分类
|
|
15
15
|
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import path from 'node:path';
|
|
18
|
-
import { spawnSync } from 'node:child_process';
|
|
19
18
|
import { fileURLToPath } from 'node:url';
|
|
20
19
|
import { inferProfile } from './infer-profile.mjs';
|
|
21
|
-
import { layout,
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
import { layout, TEMPLATES_DIR, detectEnv, SHELL_NOTES, readEnv, expandPath, readToolchain, checkToolchain, readProfile } from '../lib/project.mjs';
|
|
21
|
+
export { detectEnv, SHELL_NOTES, readEnv, expandPath, readToolchain, checkToolchain, readProfile } from '../lib/project.mjs';
|
|
22
|
+
import { capabilities, DOCNEXUS_HINT, docnexusWorkspace, docnexusConnected } from '../lib/capabilities.mjs';
|
|
23
|
+
export { docnexusRoot, docnexusWorkspace, docnexusConnected, docnexusBin, codegraphStatus } from '../lib/capabilities.mjs';
|
|
24
24
|
|
|
25
25
|
const SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
26
|
-
const PROFILE_FIELDS = ['main_branch', 'build', 'test', 'test_single', 'lint', 'config_center', 'deploy.platform', 'deploy.regions', 'job_annotation'];
|
|
27
|
-
const PROFILE_REQUIRED = ['main_branch', 'build', 'test'];
|
|
28
|
-
|
|
29
26
|
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
30
27
|
const rel = (root, p) => path.relative(root, p).split(path.sep).join('/');
|
|
31
28
|
|
|
@@ -40,25 +37,6 @@ function skillMetas() {
|
|
|
40
37
|
|
|
41
38
|
export const skillMeta = (name) => skillMetas().find((m) => m.name === name);
|
|
42
39
|
|
|
43
|
-
// CLI 位于项目根的 .docnexus/,对 Agent 返回项目相对路径。
|
|
44
|
-
export function docnexusBin(root) {
|
|
45
|
-
const bin = docnexusBinPath(root);
|
|
46
|
-
return bin ? path.relative(path.resolve(root), bin) : null;
|
|
47
|
-
}
|
|
48
|
-
// 给 agent 的命令写法(相对项目根):./.docnexus/...
|
|
49
|
-
const cmdPath = (p) => { const s = p.split(path.sep).join('/'); return s.startsWith('./') || s.startsWith('../') ? s : `./${s}`; };
|
|
50
|
-
|
|
51
|
-
// CodeGraph 代码图谱:本项目建过索引(.codegraph/)且 codegraph 命令可用。索引是否建立由用户决定,skill 不自行 codegraph init
|
|
52
|
-
export function codegraphStatus(root) {
|
|
53
|
-
const indexed = fs.existsSync(path.join(root, '.codegraph'));
|
|
54
|
-
const r = spawnSync('codegraph', ['--version'], { cwd: root, encoding: 'utf8', shell: process.platform === 'win32', env: { ...process.env, DO_NOT_TRACK: '1' }, timeout: 30000 });
|
|
55
|
-
const cli = r.status === 0;
|
|
56
|
-
return {
|
|
57
|
-
ok: indexed && cli, cli, indexed,
|
|
58
|
-
how: cli && indexed ? null : !cli ? '重跑 npx @rowansenne/workflow-run 并加 --with-codegraph(或 npm i -g @colbymchenry/codegraph)' : '在项目根运行 codegraph init 建索引(由用户决定)',
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
40
|
export function resolveTemplate(root, skill, id) {
|
|
63
41
|
const meta = skillMeta(skill);
|
|
64
42
|
if (!meta) return { ok: false, error: 'unknown', message: `skill「${skill}」没有安装(${rel(root, SKILLS_DIR)} 下找不到 ${skill}/skill.json);重跑安装并在 --skills 里带上它` };
|
|
@@ -66,36 +44,22 @@ export function resolveTemplate(root, skill, id) {
|
|
|
66
44
|
if (!t) return { ok: false, error: 'unknown', message: `skill「${skill}」没有登记模板「${id}」(见 ${skill}/skill.json 的 templates)` };
|
|
67
45
|
const lib = path.join(docnexusWorkspace(root), 'library', 'templates', skill, id);
|
|
68
46
|
const project = path.join(root, TEMPLATES_DIR, skill, id);
|
|
69
|
-
//
|
|
47
|
+
// 包内模板仅用于导入;技术 / 上线方案需要用户提供样例创建
|
|
70
48
|
const builtin = t.src ? path.join(SKILLS_DIR, skill, t.src.replace(/\.tpl$/, '')) : null;
|
|
71
49
|
const CREATE = '按 <skills>/workflow/references/templates.md「创建模板」处理:请用户给一份团队现有的同类文档作样例(本地 md 或 wiki pageId / URL),用 workflow/scripts/make-template.mjs 抽骨架,和用户确认后保存到 expected';
|
|
72
50
|
// workflow-run/templates/ 下的项目模板已不再生效(定制模板只放 DocNexus,便于共享),留着的只用来提示导入
|
|
73
51
|
const stale = fs.existsSync(project) ? rel(root, project) : null;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
ok: false, ...res, error: 'missing', expected: rel(root, lib),
|
|
86
|
-
import_from: fs.existsSync(project) ? rel(root, project) : rel(root, builtin),
|
|
87
|
-
hint: '已接入 DocNexus,只使用知识库中的模板。停下这一步告诉用户缺这份模板:可在项目根运行 npx @rowansenne/workflow-run seed-templates(或重跑安装加 --seed-templates),或经用户确认后把 import_from 按 capture 流程写入 expected(以显式分类 --category doc-template 封存)。',
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
// 未接入 DocNexus:只用 skill 自带的默认模板(只读,不能改);没有默认版本的(技术方案、上线方案)要先接入 DocNexus 才能创建
|
|
91
|
-
const res = { skill, id, docnexus: false, stale_project_file: stale };
|
|
92
|
-
if (!builtin) {
|
|
93
|
-
return {
|
|
94
|
-
ok: false, ...res, error: 'missing', need_docnexus: true, expected: rel(root, lib),
|
|
95
|
-
hint: `这份模板不随 skills 提供,自定义模板只保存在 DocNexus 知识库里(便于团队共享),本项目还没有接入 DocNexus。停下这一步告诉用户:先重跑 npx @rowansenne/workflow-run 并加 --with-docnexus 接入${stale ? `(项目里的 ${stale} 已不再生效,接入后可导入知识库)` : ''},再${CREATE}。`,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
return { ok: true, ...res, source: 'default', path: rel(root, builtin) };
|
|
52
|
+
const res = { skill, id, docnexus: docnexusConnected(root), stale_project_file: stale };
|
|
53
|
+
if (!res.docnexus) return { ok: false, ...res, error: 'missing', need_docnexus: true, expected: rel(root, lib), hint: DOCNEXUS_HINT };
|
|
54
|
+
if (fs.existsSync(lib)) return { ok: true, ...res, source: 'docnexus', path: rel(root, lib) };
|
|
55
|
+
return {
|
|
56
|
+
ok: false, ...res, error: 'missing', expected: rel(root, lib),
|
|
57
|
+
...(!builtin ? { on_demand: true } : {}),
|
|
58
|
+
import_from: stale ?? (builtin ? rel(root, builtin) : null),
|
|
59
|
+
hint: builtin
|
|
60
|
+
? '知识库缺少模板。运行 npx @rowansenne/workflow-run seed-templates 导入,或经用户确认后按 capture 流程把 import_from 写入 expected(分类 doc-template)。'
|
|
61
|
+
: `知识库还没有这份团队模板。${CREATE}(写入需用户授权,以 --category doc-template 封存)。`,
|
|
62
|
+
};
|
|
99
63
|
}
|
|
100
64
|
|
|
101
65
|
// DocNexus 对显式分类名的约束:1–80 字符,无首尾空白与控制字符(与 bin/skills.mjs 一致)
|
|
@@ -125,118 +89,12 @@ export function resolveCategory(root, skill) {
|
|
|
125
89
|
|
|
126
90
|
// 本机系统与终端:安装时检测一次,写进 workflow/profile.local.json 的 env(个人设置,不提交,各人各机器不同)。
|
|
127
91
|
// agent 写命令时按它来,不必每次自己探测(uname、$PSVersionTable)。没记录时(还没重跑安装器)当场检测系统,终端为 null
|
|
128
|
-
const OS_NAMES = { win32: 'windows', darwin: 'macos', linux: 'linux' };
|
|
129
|
-
export function detectEnv() {
|
|
130
|
-
const os = OS_NAMES[process.platform] ?? process.platform;
|
|
131
|
-
let shell;
|
|
132
|
-
if (process.platform === 'win32') {
|
|
133
|
-
shell = spawnSync('pwsh', ['-NoProfile', '-NonInteractive', '-Command', 'exit 0'], { stdio: 'ignore', timeout: 15000 }).status === 0 ? 'pwsh' : 'powershell';
|
|
134
|
-
} else shell = path.basename(process.env.SHELL || 'sh');
|
|
135
|
-
return { os, shell };
|
|
136
|
-
}
|
|
137
|
-
// 终端的写法提示:PowerShell 5.1 没有 && / ||,pwsh 7 有
|
|
138
|
-
export const SHELL_NOTES = {
|
|
139
|
-
powershell: 'Windows PowerShell 5.1:没有 && 和 ||,多条命令用 ; 分开;不要用 2>/dev/null、grep、head 等 Unix 命令',
|
|
140
|
-
pwsh: 'PowerShell 7:支持 &&;不要用 2>/dev/null、grep、head 等 Unix 命令',
|
|
141
|
-
};
|
|
142
|
-
export function readEnv() {
|
|
143
|
-
const rec = readJson(path.join(SKILLS_DIR, 'workflow', 'profile.local.json'))?.env;
|
|
144
|
-
const env = rec?.os ? { os: rec.os, shell: rec.shell ?? null, source: 'install' } : { os: OS_NAMES[process.platform] ?? process.platform, shell: null, source: 'runtime' };
|
|
145
|
-
const note = SHELL_NOTES[env.shell];
|
|
146
|
-
return { ...env, ...(note ? { note } : {}) };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 开发环境配置(toolchain):画像里的路径可能写 $JAVA_HOME / ${VAR} / %VAR% / ~,用 env_vars 与进程环境展开。
|
|
150
|
-
// env_vars 优先(画像里显式声明的是对的),缺失的键回退到本机进程环境(HOME、USERPROFILE 这类)。
|
|
151
|
-
// 展开后为空 = 变量确实没配(不是「留空」,要报出来);路径里有空格照常,不做 shell 求值。
|
|
152
|
-
const VAR_RE = /\$\{(\w+)\}|\$(\w+)|%(\w+)%/g;
|
|
153
|
-
export function expandPath(v, vars = {}, env = process.env) {
|
|
154
|
-
if (typeof v !== 'string' || !v) return { raw: v ?? '', resolved: '', missing: [] };
|
|
155
|
-
const look = (n) => (vars[n] !== undefined && vars[n] !== '' ? vars[n] : (env[n] ?? ''));
|
|
156
|
-
const home = look('HOME') || look('USERPROFILE') || '';
|
|
157
|
-
let s = v;
|
|
158
|
-
if (s === '~' || s.startsWith('~/') || s.startsWith('~\\')) s = home ? home + s.slice(1) : s;
|
|
159
|
-
const missing = [];
|
|
160
|
-
for (const m of v.matchAll(VAR_RE)) { const n = m[1] ?? m[2] ?? m[3]; if (!look(n)) missing.push(n); }
|
|
161
|
-
const resolved = s.replace(VAR_RE, (m, a, b, c) => look(a ?? b ?? c)).replace(/[\\/]+$/, '');
|
|
162
|
-
return { raw: v, resolved, missing };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// 画像里的 toolchain(profile.json)与个人覆盖(profile.local.json),逐个子字段合并,后者优先。
|
|
166
|
-
// 「没声明」和「显式写空」是两回事:本机写 "" 表示**不使用**团队那个值(用工具自己的默认配置),
|
|
167
|
-
// 所以要按 null / "" 区分来源,不能只看值真不真。
|
|
168
|
-
export function readToolchain(root) {
|
|
169
|
-
const dir = path.join(SKILLS_DIR, 'workflow');
|
|
170
|
-
const team = readJson(path.join(dir, 'profile.json')) ?? {};
|
|
171
|
-
const mine = readJson(path.join(dir, 'profile.local.json')) ?? {};
|
|
172
|
-
const declared = (o) => typeof o === 'string';
|
|
173
|
-
// 个人声明过(含 "")就以个人为准;否则用团队值。cleared 表示被个人显式留空
|
|
174
|
-
const merge = (t, m) => {
|
|
175
|
-
if (declared(m)) return m === '' ? { value: '', source: 'local', cleared: true } : { value: m, source: 'local' };
|
|
176
|
-
if (declared(t) && t !== '') return { value: t, source: 'team' };
|
|
177
|
-
return { value: '', source: null };
|
|
178
|
-
};
|
|
179
|
-
const vars = { ...(team.env_vars ?? {}), ...(mine.env_vars ?? {}) };
|
|
180
|
-
const fields = {
|
|
181
|
-
java_home: merge(team.toolchain?.java_home, mine.toolchain?.java_home),
|
|
182
|
-
'maven.settings': merge(team.toolchain?.maven?.settings, mine.toolchain?.maven?.settings),
|
|
183
|
-
'maven.repository': merge(team.toolchain?.maven?.repository, mine.toolchain?.maven?.repository),
|
|
184
|
-
gradle_user_home: merge(team.toolchain?.gradle_user_home, mine.toolchain?.gradle_user_home),
|
|
185
|
-
node: merge(team.toolchain?.node, mine.toolchain?.node),
|
|
186
|
-
};
|
|
187
|
-
const out = {};
|
|
188
|
-
const cleared = [];
|
|
189
|
-
for (const [k, { value, source, cleared: c }] of Object.entries(fields)) {
|
|
190
|
-
if (c) { cleared.push(k); continue; } // 个人明确不用这个字段:不展开、不校验、也不报缺失
|
|
191
|
-
if (value) out[k] = { ...expandPath(value, vars), source };
|
|
192
|
-
}
|
|
193
|
-
// toolchain 为空时也给出 env_vars,便于 agent 知道该用什么变量写路径
|
|
194
|
-
return { fields: out, env_vars: vars, cleared, configured: Object.keys(out).length > 0 };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 每个字段的路径在本机是否存在(写入画像前、构建跑不起来时核对用)。
|
|
198
|
-
// 只判断存在性,不判断版本对不对;current 是本机当前进程的值,便于和画像对照
|
|
199
|
-
export function checkToolchain(root, { env = process.env } = {}) {
|
|
200
|
-
const tc = readToolchain(root);
|
|
201
|
-
const check = (f) => {
|
|
202
|
-
// 变量没配时展开结果可能是别处真存在的路径($MISSING/bin 展开成 /bin),必须按「没配」算不通过
|
|
203
|
-
let exists = false;
|
|
204
|
-
try { exists = !f.missing.length && !!f.resolved && fs.existsSync(f.resolved); } catch { exists = false; }
|
|
205
|
-
const kind = exists ? (() => { try { return fs.statSync(f.resolved).isDirectory() ? 'dir' : 'file'; } catch { return 'other'; } })() : null;
|
|
206
|
-
return { ...f, exists, kind, ...(f.missing.length ? { unset_vars: f.missing } : {}), ...(f.raw !== f.resolved ? { expanded: true } : {}) };
|
|
207
|
-
};
|
|
208
|
-
const fields = Object.fromEntries(Object.entries(tc.fields).map(([k, f]) => [k, check(f)]));
|
|
209
|
-
const bad = Object.entries(fields).filter(([, f]) => !f.exists).map(([k]) => k);
|
|
210
|
-
return {
|
|
211
|
-
fields, configured: tc.configured, env_vars: tc.env_vars, ok: bad.length === 0,
|
|
212
|
-
missing: bad,
|
|
213
|
-
// 个人在 profile.local.json 里显式留空、表示不用团队那个值的字段(用工具默认配置)
|
|
214
|
-
...(tc.cleared.length ? { cleared: tc.cleared } : {}),
|
|
215
|
-
// 本机当前值:和画像对不上时,多半是画像抄的是别人的机器
|
|
216
|
-
current: { JAVA_HOME: env.JAVA_HOME ?? null, MAVEN_HOME: env.MAVEN_HOME ?? null, GRADLE_USER_HOME: env.GRADLE_USER_HOME ?? null, node: path.dirname(process.execPath) },
|
|
217
|
-
how: tc.configured ? (bad.length ? `核对 toolchain 里不存在或变量没配的字段:${bad.join('、')}(本机值见 current);改本机的那份用 profile.local.json,见 workflow/references/profile.md` : null) : '还没有配置开发环境:运行 /workflow profile 由脚本推断后确认,见 workflow/references/profile.md',
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function readProfile(root) {
|
|
222
|
-
const f = path.join(SKILLS_DIR, 'workflow', 'profile.json');
|
|
223
|
-
const profile = readJson(f);
|
|
224
|
-
const get = (k) => k.split('.').reduce((o, p) => (o == null ? undefined : o[p]), profile);
|
|
225
|
-
const isEmpty = (v) => v == null || v === '' || (Array.isArray(v) && !v.length);
|
|
226
|
-
const empty = profile ? PROFILE_FIELDS.filter((k) => isEmpty(get(k))) : PROFILE_FIELDS;
|
|
227
|
-
return {
|
|
228
|
-
path: rel(root, f), exists: !!profile, profile: profile ?? {}, empty, env: readEnv(), toolchain: readToolchain(root),
|
|
229
|
-
complete: !!profile && PROFILE_REQUIRED.every((k) => !empty.includes(k)),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
92
|
export function status(root) {
|
|
234
93
|
const p = readProfile(root);
|
|
235
|
-
const
|
|
236
|
-
const db = readJson(path.join(SKILLS_DIR, 'database-query', 'config.json'));
|
|
237
|
-
const dn = docnexusConnected(root);
|
|
94
|
+
const caps = capabilities(root);
|
|
238
95
|
const templates = skillMetas().flatMap((m) => m.templates.map((t) => resolveTemplate(root, m.name, t.id)));
|
|
239
96
|
return {
|
|
97
|
+
...caps,
|
|
240
98
|
// 已安装的 skill(--skills 子集安装时只有一部分),路由前据此判断目标 skill 是否可用
|
|
241
99
|
skills: skillMetas().map((m) => m.name),
|
|
242
100
|
// 本机系统与终端(安装时记录),写命令按它来
|
|
@@ -246,43 +104,17 @@ export function status(root) {
|
|
|
246
104
|
toolchain: p.toolchain.configured
|
|
247
105
|
? { configured: true, fields: Object.fromEntries(Object.entries(p.toolchain.fields).map(([k, v]) => [k, v.resolved])), ...(p.toolchain.cleared.length ? { cleared: p.toolchain.cleared } : {}) }
|
|
248
106
|
: { configured: false, ...(p.toolchain.cleared.length ? { cleared: p.toolchain.cleared } : {}) },
|
|
249
|
-
wiki: {
|
|
250
|
-
ok: wikiCfg?.enabled === true && !!wikiCfg?.wiki?.base_url,
|
|
251
|
-
how: '填写 md-to-wiki/wiki-config.json(enabled + base_url + api_token),或接入 wiki MCP(脚本无法探测 MCP,请另行检查可用工具)',
|
|
252
|
-
},
|
|
253
107
|
docnexus: {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
bin: dn ? cmdPath(docnexusBin(root)) : null,
|
|
257
|
-
// CLI、草稿及返回的文档路径均相对业务项目根。
|
|
258
|
-
root: '.',
|
|
259
|
-
internal: '.docnexus',
|
|
260
|
-
workspace: `${HOME}/docnexus`,
|
|
261
|
-
output_dir: HOME,
|
|
262
|
-
// 运行 DocNexus 命令的写法(在项目根执行,任何终端都一样):后面接原命令的参数;node 脚本前加 --node
|
|
263
|
-
run: `node ${(rel(root, path.join(SKILLS_DIR, 'workflow', 'scripts', 'docnexus.mjs')))}`,
|
|
264
|
-
missing_templates: dn ? templates.filter((t) => !t.ok && !t.on_demand).map((t) => t.expected) : [],
|
|
265
|
-
// workflow-run/templates/ 下已不生效的项目模板:接入 DocNexus 后导入知识库再删除
|
|
108
|
+
...caps.docnexus,
|
|
109
|
+
missing_templates: caps.docnexus.ok ? templates.filter((t) => !t.ok && !t.on_demand).map((t) => t.expected) : [],
|
|
266
110
|
stale_project_templates: templates.map((t) => t.stale_project_file).filter(Boolean),
|
|
267
|
-
// 各 skill 写入时使用的显式分类(没声明的 skill 不列出)
|
|
268
111
|
categories: Object.fromEntries(skillMetas().map((m) => [m.name, resolveCategory(root, m.name)]).filter(([, c]) => c.ok).map(([n, c]) => [n, c.category])),
|
|
269
|
-
how: dn ? '缺模板时运行 npx @rowansenne/workflow-run seed-templates' : '重跑 npx @rowansenne/workflow-run 并加 --with-docnexus',
|
|
270
112
|
},
|
|
271
113
|
// 产物位置:项目里(默认)或独立文档仓库,两种都是 docs/requirements/<需求>/<项目>/(workflow/references/requirements.md)
|
|
272
114
|
artifacts: (() => { const L = layout(root); return { mode: L.mode, ok: L.ok, dir: 'workflow-run/docs/requirements', project: L.project, ...(L.mode === 'repo' ? { repo: L.repo_rel, commit_prompt: L.commit_prompt } : {}), how: L.ok ? null : L.hint }; })(),
|
|
273
|
-
//
|
|
274
|
-
codegraph: codegraphStatus(root),
|
|
275
|
-
database: {
|
|
276
|
-
ok: !!db?.connections && Object.keys(db.connections).length > 0,
|
|
277
|
-
default: db?.default_connection ?? (db?.connections?.default ? 'default' : null),
|
|
278
|
-
connections: Object.fromEntries(Object.entries(db?.connections ?? {}).map(([k, c]) => [k, { driver: c.driver ?? 'mysql', readonly: c.readonly !== false }])),
|
|
279
|
-
how: '重跑 npx @rowansenne/workflow-run 补配数据库',
|
|
280
|
-
},
|
|
281
|
-
templates: templates.map(({ skill, id, source, path: p2, ok, on_demand, need_docnexus, expected }) => ({ skill, id, ok, source: source ?? 'missing', path: p2 ?? null, ...(on_demand ? { on_demand: true, expected } : {}), ...(need_docnexus ? { need_docnexus: true } : {}) })),
|
|
115
|
+
templates, // 包含 path / expected / hint,按共享模板规则处理,不在各 Skill 重复分支
|
|
282
116
|
// 不随 skills 提供、还没创建的模板(技术方案、上线方案):第一次生成文档时按「创建模板」引导用户创建
|
|
283
117
|
templates_to_create: templates.filter((t) => t.on_demand).map((t) => ({ skill: t.skill, id: t.id, expected: t.expected })),
|
|
284
|
-
// 没有默认版本、要先接入 DocNexus 才能创建的模板(未接入时)
|
|
285
|
-
templates_need_docnexus: templates.filter((t) => t.need_docnexus).map((t) => ({ skill: t.skill, id: t.id })),
|
|
286
118
|
};
|
|
287
119
|
}
|
|
288
120
|
|
|
@@ -18,7 +18,8 @@ import fs from 'node:fs';
|
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import { readProfile
|
|
21
|
+
import { readProfile } from '../lib/project.mjs';
|
|
22
|
+
import { resolveTemplate } from './resolve.mjs';
|
|
22
23
|
|
|
23
24
|
const git = (root, args) => spawnSync('git', ['-c', 'core.quotepath=false', ...args], { cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
24
25
|
const toPosix = (p) => p.split(path.sep).join('/');
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 完整流程的运行记录统一保存在需求 meta.json;读取不创建文件,快速通道不调用写命令。
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { STATUS_LABELS, runState, startRun, changeRun } from '../lib/runs.mjs';
|
|
9
|
+
export { FLOWS, STATUS_LABELS, allRuns, branchRuns, runState, startRun, changeRun } from '../lib/runs.mjs';
|
|
10
|
+
const branchOf = (root) => spawnSync('git', ['symbolic-ref', '--short', 'HEAD'], { cwd: root, encoding: 'utf8' }).stdout?.trim() || null;
|
|
11
|
+
|
|
12
|
+
function main(argv) {
|
|
13
|
+
const opts = {}; const args = [...argv]; const action = args.shift();
|
|
14
|
+
try {
|
|
15
|
+
while (args.length) {
|
|
16
|
+
const key = args.shift();
|
|
17
|
+
if (!['--root', '--branch', '--dir', '--id', '--kind', '--size', '--input', '--expect-revision'].includes(key) || !args.length) throw new Error(`未知或缺少值的参数:${key}`);
|
|
18
|
+
opts[key.slice(2)] = args.shift();
|
|
19
|
+
}
|
|
20
|
+
const root = path.resolve(opts.root ?? '.'); const branch = opts.branch ?? branchOf(root);
|
|
21
|
+
if (!branch) throw new Error('无法确定当前分支,请用 --branch 指定');
|
|
22
|
+
const input = opts.input ? JSON.parse(fs.readFileSync(opts.input === '-' ? 0 : path.resolve(root, opts.input), 'utf8')) : {};
|
|
23
|
+
let out;
|
|
24
|
+
if (action === 'show') {
|
|
25
|
+
out = runState(root, branch);
|
|
26
|
+
if (opts.id) {
|
|
27
|
+
out.current = out.runs.find((r) => r.id === opts.id) ?? null;
|
|
28
|
+
if (!out.current) throw new Error('当前分支没有这个运行');
|
|
29
|
+
out.next = `${STATUS_LABELS[out.current.status]}:${out.current.stage}(${out.current.id})`;
|
|
30
|
+
}
|
|
31
|
+
} else if (action === 'start') out = startRun(root, { branch, kind: opts.kind, size: opts.size, input, dir: opts.dir });
|
|
32
|
+
else out = changeRun(root, { branch, id: opts.id, revision: opts['expect-revision'] === undefined ? null : Number(opts['expect-revision']), input, action });
|
|
33
|
+
console.log(JSON.stringify(out, null, 2)); return 0;
|
|
34
|
+
} catch (e) { console.error(e.message); return 1; }
|
|
35
|
+
}
|
|
36
|
+
const invoked = process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
37
|
+
if (invoked) process.exit(main(process.argv.slice(2)));
|
|
@@ -15,7 +15,7 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import { spawnSync } from 'node:child_process';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import { readProfile } from '
|
|
18
|
+
import { readProfile } from '../lib/project.mjs';
|
|
19
19
|
|
|
20
20
|
const SKILLS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
21
21
|
const EXT = /\.(java|kt|scala|groovy|go|py|ts|js|xml|sql)$/i;
|
|
@@ -50,10 +50,6 @@ export function target(name) {
|
|
|
50
50
|
return t ? { skill, id: t.id } : null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// 未接入 DocNexus:默认模板只读,不能改也不能新建
|
|
54
|
-
const NEED_DOCNEXUS = '默认模板随 skills 提供、只读不能改;自定义模板只保存在 DocNexus 知识库里,便于团队共享。告诉用户先重跑 npx @rowansenne/workflow-run 并加 --with-docnexus 接入,再修改或创建模板。';
|
|
55
|
-
const needDocNexus = (t, r) => ({ code: 2, out: { ok: false, skill: t.skill, id: t.id, docnexus: false, need_docnexus: true, current: r.ok ? r.path : null, hint: NEED_DOCNEXUS } });
|
|
56
|
-
|
|
57
53
|
const draftPath = (root, { skill, id }) => path.join(root, 'workflow-run', 'tmp', 'template-draft', skill, id);
|
|
58
54
|
|
|
59
55
|
// 草稿本身的格式检查:错误挡住保存,警告给用户看
|
|
@@ -123,7 +119,7 @@ export function draft(root, t, { from = null, force = false, stdin = () => fs.re
|
|
|
123
119
|
const file = draftPath(root, t);
|
|
124
120
|
const { r, text } = current(root, t);
|
|
125
121
|
if (r.error === 'unknown') return { code: 1, out: { ok: false, skill: t.skill, id: t.id, message: r.message } };
|
|
126
|
-
if (!r.docnexus) return
|
|
122
|
+
if (!r.docnexus) return { code: 2, out: r };
|
|
127
123
|
const base = { skill: t.skill, id: t.id, draft: rel(root, file), source: r.ok ? r.source : null, current: r.ok ? r.path : null, docnexus: true, expected: r.ok ? r.path : r.expected };
|
|
128
124
|
if (fs.existsSync(file) && !force && !from) return { code: 0, out: { ok: true, ...base, created: false, note: '草稿已存在,沿用上次的草稿;要从当前模板重来加 --force' } };
|
|
129
125
|
let content;
|
|
@@ -169,7 +165,7 @@ export function save(root, t, file = null) {
|
|
|
169
165
|
if (c.code) return c;
|
|
170
166
|
if (c.out.unchanged) return { code: 0, out: { ok: true, skill: t.skill, id: t.id, saved: null, note: '草稿与当前模板相同,没有写入' } };
|
|
171
167
|
const r = resolveTemplate(root, t.skill, t.id);
|
|
172
|
-
if (!r.docnexus) return
|
|
168
|
+
if (!r.docnexus) return { code: 2, out: r };
|
|
173
169
|
return {
|
|
174
170
|
code: 2, out: {
|
|
175
171
|
ok: false, skill: t.skill, id: t.id, docnexus: true, draft: c.out.draft, expected: r.ok ? r.path : r.expected,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// 项目文档视图:把 workflow-run/docs/requirements/<需求>/ 下的产物生成一个本地静态页面,
|
|
3
|
-
//
|
|
3
|
+
// 按需求浏览方案、交付说明、审查报告(含复审修复情况)、技术方案、上线方案,正文直接渲染。
|
|
4
4
|
// 只读产物;页面是单个 HTML 文件,数据内嵌,不联网、不上报。默认写到 workflow-run/tmp/view/index.html(tmp 不入库)。
|
|
5
5
|
//
|
|
6
6
|
// 用法(在项目根执行;--root 可指定项目根):
|
|
@@ -11,20 +11,22 @@ import fs from 'node:fs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { KINDS } from '../lib/artifact-model.mjs';
|
|
15
|
+
import { loadWorkspace } from '../lib/workspace-read.mjs';
|
|
16
|
+
import { groupRequirements } from '../lib/requirement-read.mjs';
|
|
17
|
+
import { REQ_DIR } from '../lib/project.mjs';
|
|
18
|
+
import { buildReport } from './report.mjs';
|
|
17
19
|
|
|
18
20
|
const DEFAULT_OUT = 'workflow-run/tmp/view/index.html';
|
|
19
21
|
|
|
20
22
|
// 页面数据:按需求分组
|
|
21
|
-
export function viewData(root) {
|
|
23
|
+
export function viewData(root, workspace = loadWorkspace(root, { allProjects: true })) {
|
|
22
24
|
// 连同其他项目(服务)的产物一起看,一个需求下按项目区分(文档仓库里常见;两种模式目录结构相同)
|
|
23
|
-
const L = layout
|
|
25
|
+
const L = workspace.layout;
|
|
24
26
|
const reqDirOf = (slug) => (L.mode === 'repo' ? `${L.repo_name}:docs/requirements/${slug}` : `${REQ_DIR}/${slug}`);
|
|
25
|
-
const metas = new Map(
|
|
27
|
+
const metas = new Map(groupRequirements(workspace.requirements).map((r) => [r.slug, { name: r.name, dir: reqDirOf(r.slug), branches: [...new Set(r.projects.flatMap((p) => p.meta.branches ?? []))], meta: r.projects.find((p) => p.project === L.project)?.meta ?? r.projects[0]?.meta ?? {}, runs: r.projects.flatMap((p) => workspace.runs.filter((run) => run.dir === p.dir).map(({ requirement, dir, ...run }) => run)) }]));
|
|
26
28
|
const groups = new Map();
|
|
27
|
-
for (const a of
|
|
29
|
+
for (const a of workspace.artifacts) {
|
|
28
30
|
const id = a.requirement ? `req:${a.requirement}` : `branch:${a.branch ?? '(无分支)'}`;
|
|
29
31
|
if (!groups.has(id)) {
|
|
30
32
|
const m = a.requirement ? metas.get(a.requirement) : null;
|
|
@@ -36,17 +38,23 @@ export function viewData(root) {
|
|
|
36
38
|
const g = groups.get(id);
|
|
37
39
|
if (a.branch) g.branches.add(a.branch);
|
|
38
40
|
if (a.project) g.projects.add(a.project);
|
|
39
|
-
const text =
|
|
40
|
-
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date:
|
|
41
|
-
if (a.kind === 'review') item.review =
|
|
42
|
-
if (a.kind === 'delivery') item.delivery =
|
|
43
|
-
if (a.kind === 'review' || a.kind === 'delivery') item.docnexus =
|
|
44
|
-
item.knowledge =
|
|
41
|
+
const text = a.text;
|
|
42
|
+
const item = { kind: a.kind, label: a.label, path: a.path, cite: a.cite ?? a.path, project: a.project ?? null, file: a.project && L.mode === 'repo' ? `${a.project}/${path.basename(a.path)}` : path.basename(a.path), date: a.date, version: a.version ?? 1, branch: a.branch, wiki: a.wiki, text };
|
|
43
|
+
if (a.kind === 'review') item.review = a.review;
|
|
44
|
+
if (a.kind === 'delivery') item.delivery = a.delivery;
|
|
45
|
+
if (a.kind === 'review' || a.kind === 'delivery') item.docnexus = a.capture;
|
|
46
|
+
item.knowledge = a.knowledge; // 产物里「DocNexus:docnexus/library/<path>」行:沉淀到的知识文档
|
|
45
47
|
g.artifacts.push(item);
|
|
46
48
|
}
|
|
49
|
+
// 完整流程在首次产物前也可能已保存状态,不能因为没有 Markdown 就隐藏需求。
|
|
50
|
+
for (const [slug, m] of metas) {
|
|
51
|
+
const id = `req:${slug}`;
|
|
52
|
+
if (m.runs.length && !groups.has(id)) groups.set(id, { id, name: m.name, dir: m.dir, named_by: m.meta.named_by ?? null, branches: new Set(m.branches), projects: new Set(m.runs.map((r) => r.project)), artifacts: [] });
|
|
53
|
+
if (groups.has(id)) groups.get(id).runs = m.runs;
|
|
54
|
+
}
|
|
47
55
|
const list = [...groups.values()].map((g) => {
|
|
48
56
|
const latest = (k) => g.artifacts.filter((a) => a.kind === k).at(-1)?.date ?? null;
|
|
49
|
-
const dates = g.artifacts.map((a) => a.date).filter(Boolean).sort();
|
|
57
|
+
const dates = [...g.artifacts.map((a) => a.date), ...(g.runs ?? []).map((r) => r.updated_at?.slice(0, 10))].filter(Boolean).sort();
|
|
50
58
|
// 沉淀到的知识文档:汇总各产物的「DocNexus:」行,去重,注明来自哪些产物
|
|
51
59
|
const docnexus = new Map();
|
|
52
60
|
for (const a of g.artifacts) for (const k of a.knowledge) {
|
|
@@ -69,7 +77,8 @@ export function viewData(root) {
|
|
|
69
77
|
generated: new Date().toISOString(),
|
|
70
78
|
kinds: KINDS.map(({ kind, label }) => ({ kind, label })),
|
|
71
79
|
requirements: list,
|
|
72
|
-
report: buildReport(root),
|
|
80
|
+
report: buildReport(root, { workspace }),
|
|
81
|
+
...(workspace.diagnostics.length ? { diagnostics: workspace.diagnostics } : {}),
|
|
73
82
|
};
|
|
74
83
|
}
|
|
75
84
|
|
|
@@ -191,6 +200,7 @@ function md(src) {
|
|
|
191
200
|
}
|
|
192
201
|
|
|
193
202
|
const pct = (v) => (v == null ? '—' : v + '%');
|
|
203
|
+
const dnLabel = { captured: '已沉淀', not_captured: '未沉淀', not_asked: '未询问', not_needed: '无需沉淀', not_connected: '未接入' };
|
|
194
204
|
function overview() {
|
|
195
205
|
const r = D.report; const v = r.review;
|
|
196
206
|
const cards = [
|
|
@@ -201,7 +211,8 @@ function overview() {
|
|
|
201
211
|
['复审修复率', pct(v.fix_rate)],
|
|
202
212
|
['沉淀率', pct(r.capture.capture_rate)],
|
|
203
213
|
];
|
|
204
|
-
|
|
214
|
+
const warnings = (D.diagnostics ?? []).map((d) => '<p>' + esc(d.path + ':' + d.message) + '</p>').join('');
|
|
215
|
+
return (warnings ? '<section class="block"><h3>数据检查</h3>' + warnings + '</section>' : '') + '<div class="overview">' + cards.map(([k, v]) => '<div class="card"><div class="k">' + k + '</div><div class="v">' + esc(v) + '</div></div>').join('') + '</div>';
|
|
205
216
|
}
|
|
206
217
|
|
|
207
218
|
let current = null; let currentDoc = null;
|
|
@@ -218,10 +229,17 @@ function reviewBlock(r) {
|
|
|
218
229
|
const row = (a) => { const v = a.review; const f = v.follow_up;
|
|
219
230
|
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (v.is_rereview ? '复审' : '首次') + '</td>'
|
|
220
231
|
+ '<td><span class="lv c">' + v.levels.CRITICAL + '</span> <span class="lv h">' + v.levels.HIGH + '</span> ' + v.levels.MEDIUM + ' / ' + v.levels.LOW + '</td>'
|
|
221
|
-
+ '<td>✅ ' + v.verified + ' · ⚠ ' + v.pending + '</td><td>' + (v.is_rereview ? '<span class="lv f">已修复 ' + f.fixed + '</span> 仍存在 ' + f.open + ' · 不再适用 ' + f.obsolete : '—') + '</td><td>' + esc(a.docnexus
|
|
232
|
+
+ '<td>✅ ' + v.verified + ' · ⚠ ' + v.pending + '</td><td>' + (v.is_rereview ? '<span class="lv f">已修复 ' + f.fixed + '</span> 仍存在 ' + f.open + ' · 不再适用 ' + f.obsolete : '—') + '</td><td>' + esc(dnLabel[a.docnexus] ?? '—') + '</td></tr>'; };
|
|
222
233
|
return '<section class="block"><h3>审查记录</h3><table><thead><tr><th>报告</th><th>日期</th><th>轮次</th><th>CRITICAL · HIGH · MEDIUM / LOW</th><th>复核</th><th>与上次对比</th><th>DocNexus</th></tr></thead><tbody>' + rs.map(row).join('') + '</tbody></table></section>';
|
|
223
234
|
}
|
|
224
235
|
|
|
236
|
+
function runBlock(r) {
|
|
237
|
+
if (!r.runs?.length) return '';
|
|
238
|
+
const labels = { running: '进行中', waiting_user: '等待用户', blocked: '受阻', completed: '已完成', cancelled: '已取消' };
|
|
239
|
+
const stepLabels = { pending: '未开始', in_progress: '进行中', completed: '已完成', skipped: '已跳过' };
|
|
240
|
+
return '<section class="block"><h3>流程运行状态</h3><table><thead><tr><th>运行 / 分支</th><th>阶段</th><th>状态</th><th>步骤进度</th><th>等待 / 阻塞 / 完成时间</th></tr></thead><tbody>' + r.runs.map((run) => '<tr><td>' + esc(run.project + ' · ' + run.kind + ' · ' + run.id) + '<br>' + esc(run.branch) + '</td><td>' + esc(run.stage) + '</td><td>' + esc(labels[run.status] ?? run.status) + '</td><td>' + Object.entries(run.steps).map(([name, s]) => esc(name + ':' + (stepLabels[s.status] ?? s.status))).join('<br>') + '</td><td>' + esc(run.waiting_for ?? run.reason ?? run.completed_at ?? '') + '</td></tr>').join('') + '</tbody></table></section>';
|
|
241
|
+
}
|
|
242
|
+
|
|
225
243
|
function knowledgeBlock(r) {
|
|
226
244
|
if (!r.docnexus.length) return '';
|
|
227
245
|
return '<section class="block"><h3>沉淀到的知识文档</h3><table><thead><tr><th>知识文档</th><th>来自产物</th></tr></thead><tbody>' + r.docnexus.map((d) => '<tr><td><code>' + esc(d.path) + '</code></td><td>' + d.from.map((f) => '<a href="#" data-doc="' + esc(f.path) + '">' + esc(f.label + ' · ' + f.file) + '</a>').join('<br>') + '</td></tr>').join('') + '</tbody></table></section>';
|
|
@@ -245,7 +263,7 @@ function deliveryBlock(r) {
|
|
|
245
263
|
const ds = r.artifacts.filter((a) => a.kind === 'delivery');
|
|
246
264
|
if (!ds.length) return '';
|
|
247
265
|
return '<section class="block"><h3>交付记录</h3><table><thead><tr><th>交付说明</th><th>日期</th><th>类型</th><th>规模</th><th>确认的决定</th><th>未验证项</th><th>DocNexus</th></tr></thead><tbody>' + ds.map((a) => { const d = a.delivery;
|
|
248
|
-
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (d.type === 'bug' ? '缺陷修复' : d.type === 'feature' ? '功能开发' : '—') + '</td><td>' + (d.scale || '—') + '</td><td>' + d.decisions + '</td><td>' + (d.has_unverified ? '<span class="lv h">有</span>' : '无') + '</td><td>' + esc(a.docnexus
|
|
266
|
+
return '<tr><td><a href="#" data-doc="' + esc(a.path) + '">' + esc(a.file) + '</a></td><td>' + esc(a.date || '') + '</td><td>' + (d.type === 'bug' ? '缺陷修复' : d.type === 'feature' ? '功能开发' : '—') + '</td><td>' + (d.scale || '—') + '</td><td>' + d.decisions + '</td><td>' + (d.has_unverified ? '<span class="lv h">有</span>' : '无') + '</td><td>' + esc(dnLabel[a.docnexus] ?? '—') + '</td></tr>'; }).join('') + '</tbody></table></section>';
|
|
249
267
|
}
|
|
250
268
|
|
|
251
269
|
function select(id, docPath) {
|
|
@@ -258,9 +276,11 @@ function select(id, docPath) {
|
|
|
258
276
|
const order = D.kinds.map((k) => k.kind);
|
|
259
277
|
const files = '<div class="files">' + r.artifacts.slice().sort((x, y) => order.indexOf(x.kind) - order.indexOf(y.kind)).map((a) => '<button data-doc="' + esc(a.path) + '">' + esc(a.label) + ' · ' + esc(a.file) + '</button>').join('') + '</div>';
|
|
260
278
|
$('#main').innerHTML = overview() + '<h2>' + esc(r.name) + '</h2><div class="sub">' + esc(r.dir ?? '') + (r.projects.length ? ' · 项目 ' + esc(r.projects.join('、')) : '') + ' · 分支 ' + esc(r.branches.join('、') || '无') + (r.named_by === 'branch' ? ' · <span class="tag">需求名由分支名生成,可在 meta.json 改</span>' : '') + '</div>'
|
|
261
|
-
+ pipeline + reviewBlock(r) + deliveryBlock(r) + knowledgeBlock(r) + files + '<div class="doc" id="doc"></div>';
|
|
279
|
+
+ pipeline + runBlock(r) + reviewBlock(r) + deliveryBlock(r) + knowledgeBlock(r) + files + '<div class="doc" id="doc"></div>';
|
|
262
280
|
document.querySelectorAll('[data-doc]').forEach((el) => (el.onclick = (e) => { e.preventDefault(); showDoc(r, el.dataset.doc); }));
|
|
263
|
-
|
|
281
|
+
const selected = docPath || (latest('review') || r.artifacts.at(-1))?.path;
|
|
282
|
+
if (selected) showDoc(r, selected);
|
|
283
|
+
else $('#doc').innerHTML = '<div class="empty">运行进度已保存,尚未生成文档。</div>';
|
|
264
284
|
renderList();
|
|
265
285
|
}
|
|
266
286
|
|