@godv61/dsh-task-engine 0.19.1 → 0.20.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/hooks/commit-msg CHANGED
@@ -1,275 +1,428 @@
1
1
  #!/usr/bin/env node
2
- // dsh-task-engine commit-msg hook — mechanical commit gate.
3
- //
4
- // Installed into `.git/hooks/commit-msg` by `dev_task` (operation=install_hook).
5
- // Self-contained mirror of src/engine.ts (`commitCheckpoint` + `validateCommitMessage`
6
- // + `guardSatisfied`). KEEP IN SYNC with that source when the gate semantics change.
7
- //
8
- // Policy: refuse any `git commit` that is not backed by a dev_task record sitting
9
- // at a commit-allowed checkpoint whose message matches the configured pattern.
10
- 'use strict'
11
2
 
12
- const fs = require('node:fs')
13
- const path = require('node:path')
14
- const { execSync } = require('node:child_process')
3
+ "use strict";
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
+ // If the importer is in node compatibility mode or this is not an ESM
20
+ // file that has been converted to a CommonJS file using a Babel-
21
+ // compatible transform (i.e. "__esModule" has not been set), then set
22
+ // "default" to the CommonJS "module.exports" for node compatibility.
23
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
+ mod
25
+ ));
15
26
 
16
- const messageFile = process.argv[2]
27
+ // src/hook.ts
28
+ var import_node_child_process = require("node:child_process");
29
+ var import_node_fs = __toESM(require("node:fs"), 1);
30
+ var import_node_path = __toESM(require("node:path"), 1);
17
31
 
18
- function refuse(reason) {
19
- console.error('')
20
- console.error('dsh-task-engine 提交门禁拒绝: ' + reason)
21
- console.error('请先用 dev_task 走完前置阶段(或修正提交消息),再提交。')
22
- console.error('')
23
- process.exit(1)
32
+ // src/engine.ts
33
+ function artifactsForStage(stage, config2) {
34
+ return config2.artifacts.filter((artifact) => artifact.stage === stage);
24
35
  }
25
-
26
- function readJson(abs) {
27
- try {
28
- return JSON.parse(fs.readFileSync(abs, 'utf8'))
29
- } catch {
30
- return undefined
36
+ function todosBlockers(state2) {
37
+ if (state2.items.length === 0) return ["no implementation items"];
38
+ const blockers = [];
39
+ for (const item of state2.items) {
40
+ if (item.status !== "done") {
41
+ blockers.push(`item "${item.id}" is ${item.status}`);
42
+ continue;
43
+ }
44
+ if (item.review === void 0) {
45
+ blockers.push(`item "${item.id}" is done but has no two-stage review`);
46
+ continue;
47
+ }
48
+ if (item.review.spec.outcome !== "pass") blockers.push(`item "${item.id}" spec review is not pass`);
49
+ if (item.review.quality.outcome !== "pass") blockers.push(`item "${item.id}" quality review is not pass`);
31
50
  }
51
+ return blockers;
32
52
  }
33
-
34
- // Mirrors src/workflows.ts FLOW_CONFIGS — keep in sync.
35
- const FLOWS = {
36
- standard: {
37
- stages: ['需求评审', '设计', '开发', '交付', '代码审核', '完成'],
38
- start_stage: '需求评审',
39
- transitions: [
40
- { from: '需求评审', to: '设计', requires: ['requirement_confirmation', 'artifacts_present'] },
41
- { from: '设计', to: '开发', requires: ['solution_confirmation', 'artifacts_present'] },
42
- { from: '开发', to: '交付', requires: ['todos_done'] },
43
- { from: '交付', to: '代码审核', requires: ['verified'] },
44
- { from: '代码审核', to: '完成', requires: ['review_passed', 'artifacts_present'] },
45
- ],
46
- artifacts: [
47
- { stage: '需求评审', id: 'requirement', name: '需求说明', fields: ['scope', 'acceptance_criteria'] },
48
- { stage: '设计', id: 'design', name: '设计文档', fields: ['approach', 'risks', 'impact'] },
49
- { stage: '代码审核', id: 'review', name: '评审记录', fields: ['conclusion', 'issues'] },
50
- ],
51
- commit: {
52
- policy: 'task',
53
- message_pattern: '^【(\\S+)】【(?:TASK|T\\d+)】.+',
54
- message_hint: '【<module>】【TASK/T1】说明',
55
- checkpoints: ['交付'],
56
- file_scope: true,
57
- },
58
- high_risk_requires_verification: true,
59
- },
60
- agile: {
61
- stages: ['需求', '开发', '交付', '审查'],
62
- start_stage: '需求',
63
- transitions: [
64
- { from: '需求', to: '开发', requires: ['requirement_confirmation'] },
65
- { from: '开发', to: '交付', requires: ['todos_done'] },
66
- { from: '交付', to: '审查', requires: [] },
67
- ],
68
- artifacts: [
69
- { stage: '需求', id: 'requirement', name: '需求说明', fields: ['scope'] },
70
- ],
71
- commit: {
72
- policy: 'item',
73
- message_pattern: '^【(\\S+)】【T\\d+】.+',
74
- message_hint: '【<module>】【T1】说明',
75
- checkpoints: ['交付'],
76
- file_scope: true,
77
- },
78
- high_risk_requires_verification: false,
79
- },
80
- minimal: {
81
- stages: ['开发', '交付'],
82
- start_stage: '开发',
83
- transitions: [
84
- { from: '开发', to: '交付', requires: ['todos_done'] },
85
- ],
86
- artifacts: [],
87
- commit: {
88
- policy: 'task',
89
- message_pattern: '',
90
- message_hint: '无格式要求',
91
- checkpoints: ['交付'],
92
- file_scope: false,
93
- },
94
- high_risk_requires_verification: false,
95
- },
96
- }
97
-
98
- function loadWorkflow(parsed) {
99
- const flow = parsed && typeof parsed.flow === 'string' ? parsed.flow : 'standard'
100
- return FLOWS[flow] || FLOWS.standard
101
- }
102
-
103
- // Mirrors engine.guardSatisfied.
104
- function guardSatisfied(guard, state, config) {
53
+ function guardSatisfied(guard, state2, config2) {
105
54
  switch (guard) {
106
- case 'requirement_confirmation': return state.requirement_confirmed === true
107
- case 'solution_confirmation': return state.solution_confirmed === true
108
- case 'todos_done':
109
- return Array.isArray(state.items) && state.items.length > 0 && state.items.every(item => item.status === 'done')
110
- case 'verified':
111
- if (!state.verification || state.verification.passed !== true) return false
112
- return config.high_risk_requires_verification && state.risk_level === 'high_risk'
113
- ? Array.isArray(state.verification.evidence) && state.verification.evidence.length > 0
114
- : true
115
- case 'review_passed': return state.review && state.review.outcome === 'pass'
116
- case 'artifacts_present': {
117
- const recorded = state.artifacts || {}
118
- return (config.artifacts || []).filter(artifact => artifact.stage === state.stage).every(artifact => {
119
- const fields = recorded[artifact.id]
120
- if (!fields) return false
121
- return (artifact.fields || []).every(field => typeof fields[field] === 'string' && fields[field].trim() !== '')
122
- })
55
+ case "requirement_confirmation":
56
+ return state2.requirement_confirmed;
57
+ case "solution_confirmation":
58
+ return state2.solution_confirmed;
59
+ case "todos_done":
60
+ return todosBlockers(state2).length === 0;
61
+ case "verified": {
62
+ if (!state2.verification.passed) return false;
63
+ if (config2.high_risk_requires_verification && state2.risk_level === "high_risk") {
64
+ const receipt = state2.verification.receipt;
65
+ return receipt !== void 0 && receipt.exit_code === 0 && !receipt.timed_out && !receipt.aborted;
66
+ }
67
+ return true;
68
+ }
69
+ case "review_passed":
70
+ return state2.review.outcome === "pass";
71
+ case "artifacts_present": {
72
+ const required = artifactsForStage(state2.stage, config2);
73
+ return required.every((artifact) => {
74
+ const recorded = state2.artifacts[artifact.id];
75
+ if (recorded === void 0) return false;
76
+ return artifact.fields.every((field) => recorded[field] !== void 0 && recorded[field].trim() !== "");
77
+ });
123
78
  }
124
- default: return false
125
79
  }
126
80
  }
127
-
128
- // Mirrors engine.commitCheckpoint.
129
- function commitCheckpoint(state, config) {
130
- const { policy, checkpoints } = config.commit
131
- if (policy === 'manual') {
132
- return { allowed: false, reason: 'commit policy is manual — await an explicit user instruction' }
81
+ function validateCommitMessage(message2, config2) {
82
+ if (config2.commit.message_pattern === "") return { ok: true };
83
+ const re = new RegExp(config2.commit.message_pattern);
84
+ if (re.test(message2)) return { ok: true };
85
+ return {
86
+ ok: false,
87
+ errors: [`commit summary "${message2}" does not match ${config2.commit.message_hint}`]
88
+ };
89
+ }
90
+ function commitCheckpoint(state2, config2) {
91
+ const { policy, checkpoints } = config2.commit;
92
+ if (policy === "manual") {
93
+ return { allowed: false, reason: "commit policy is manual \u2014 await an explicit user instruction" };
133
94
  }
134
- if (checkpoints.includes(state.stage)) {
135
- const ready = config.transitions
136
- .filter(transition => transition.from === state.stage)
137
- .every(transition => (transition.requires ?? []).every(guard => guardSatisfied(guard, state, config)))
138
- if (!ready) return { allowed: false, reason: 'checkpoint stage reached but its outgoing guards are unmet' }
139
- return { allowed: true, label: 'TASK' }
95
+ if (checkpoints.includes(state2.stage)) {
96
+ const ready = config2.transitions.filter((transition) => transition.from === state2.stage).every((transition) => (transition.requires ?? []).every((g) => guardSatisfied(g, state2, config2)));
97
+ if (!ready) {
98
+ return { allowed: false, reason: "checkpoint stage reached but its outgoing guards are unmet" };
99
+ }
100
+ return { allowed: true, label: "TASK" };
140
101
  }
141
- if (policy === 'item') {
142
- const done = (state.items ?? []).filter(item => item.status === 'done').length
143
- if (done > 0) return { allowed: true, label: 'T' + done }
102
+ if (policy === "item") {
103
+ const done = state2.items.filter((item) => item.status === "done").length;
104
+ if (done > 0) {
105
+ return { allowed: true, label: `T${done}` };
106
+ }
144
107
  }
145
- return { allowed: false, reason: 'no commit is due at this stage under the current policy' }
108
+ return { allowed: false, reason: "no commit is due at this stage under the current policy" };
109
+ }
110
+ function isEngineMeta(file) {
111
+ return /^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(file);
112
+ }
113
+ function checkFileScope(state2, committing, config2) {
114
+ if (!config2.commit.file_scope) return { ok: true, outside: [], noScope: false };
115
+ if (state2.files.length === 0) return { ok: false, outside: [], noScope: true };
116
+ const outside = committing.filter((file) => !state2.files.includes(file) && !isEngineMeta(file));
117
+ return outside.length === 0 ? { ok: true, outside: [], noScope: false } : { ok: false, outside, noScope: false };
146
118
  }
147
119
 
148
- // Mirrors engine.validateCommitMessage.
149
- function validateCommitMessage(message, config) {
150
- if (config.commit.message_pattern === '') return { ok: true }
151
- try {
152
- return new RegExp(config.commit.message_pattern).test(message)
153
- ? { ok: true }
154
- : { ok: false, reason: 'commit summary does not match ' + config.commit.message_hint }
155
- } catch {
156
- return { ok: false, reason: 'commit.message_pattern is not a valid regular expression' }
120
+ // src/workflows.ts
121
+ var STANDARD = {
122
+ stages: ["\u9700\u6C42\u8BC4\u5BA1", "\u8BBE\u8BA1", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u4EE3\u7801\u5BA1\u6838", "\u5B8C\u6210"],
123
+ start_stage: "\u9700\u6C42\u8BC4\u5BA1",
124
+ transitions: [
125
+ { from: "\u9700\u6C42\u8BC4\u5BA1", to: "\u8BBE\u8BA1", requires: ["requirement_confirmation", "artifacts_present"] },
126
+ { from: "\u8BBE\u8BA1", to: "\u5F00\u53D1", requires: ["solution_confirmation", "artifacts_present"] },
127
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
128
+ { from: "\u4EA4\u4ED8", to: "\u4EE3\u7801\u5BA1\u6838", requires: ["verified"] },
129
+ { from: "\u4EE3\u7801\u5BA1\u6838", to: "\u5B8C\u6210", requires: ["review_passed", "artifacts_present"] }
130
+ ],
131
+ artifacts: [
132
+ { stage: "\u9700\u6C42\u8BC4\u5BA1", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope", "acceptance_criteria"] },
133
+ { stage: "\u8BBE\u8BA1", id: "design", name: "\u8BBE\u8BA1\u6587\u6863", fields: ["approach", "risks", "impact"] },
134
+ { stage: "\u4EE3\u7801\u5BA1\u6838", id: "review", name: "\u8BC4\u5BA1\u8BB0\u5F55", fields: ["conclusion", "issues"] }
135
+ ],
136
+ commit: {
137
+ policy: "task",
138
+ message_pattern: "^\u3010(\\S+)\u3011\u3010(?:TASK|T\\d+)\u3011.+",
139
+ message_hint: "\u3010<task_id>\u3011\u3010TASK/T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id\uFF08\u5982 GREET-001\uFF09\uFF0C\u5199\u7ED3\u679C\u4E0D\u5199\u7A7A\u6CDB\u52A8\u4F5C",
140
+ checkpoints: ["\u4EA4\u4ED8"],
141
+ file_scope: true
142
+ },
143
+ high_risk_requires_verification: true,
144
+ stage_bindings: {
145
+ "\u9700\u6C42\u8BC4\u5BA1": { skills: ["requirement-analysis"], rules: ["security-redlines"] },
146
+ "\u8BBE\u8BA1": { skills: ["solution-design"] },
147
+ "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
148
+ "\u4EA4\u4ED8": { skills: ["code-verify", "code-commit"], rules: ["coding-conventions", "commit-conventions"] },
149
+ "\u4EE3\u7801\u5BA1\u6838": { skills: ["code-review"], rules: ["security-redlines"] }
157
150
  }
151
+ };
152
+ var AGILE = {
153
+ stages: ["\u9700\u6C42", "\u5F00\u53D1", "\u4EA4\u4ED8", "\u5BA1\u67E5"],
154
+ start_stage: "\u9700\u6C42",
155
+ transitions: [
156
+ { from: "\u9700\u6C42", to: "\u5F00\u53D1", requires: ["requirement_confirmation"] },
157
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] },
158
+ { from: "\u4EA4\u4ED8", to: "\u5BA1\u67E5", requires: [] }
159
+ ],
160
+ artifacts: [
161
+ { stage: "\u9700\u6C42", id: "requirement", name: "\u9700\u6C42\u8BF4\u660E", fields: ["scope"] }
162
+ ],
163
+ commit: {
164
+ policy: "item",
165
+ message_pattern: "^\u3010(\\S+)\u3011\u3010T\\d+\u3011.+",
166
+ message_hint: "\u3010<task_id>\u3011\u3010T1\u3011\u8BF4\u660E \u2014\u2014 \u7B2C\u4E00\u6BB5\u586B\u672C\u4EFB\u52A1 id",
167
+ checkpoints: ["\u4EA4\u4ED8"],
168
+ file_scope: true
169
+ },
170
+ high_risk_requires_verification: false,
171
+ stage_bindings: {
172
+ "\u9700\u6C42": { skills: ["requirement-analysis"] },
173
+ "\u5F00\u53D1": { skills: ["code-implement"], rules: ["coding-conventions"] },
174
+ "\u4EA4\u4ED8": { skills: ["code-verify", "code-commit"], rules: ["commit-conventions"] },
175
+ "\u5BA1\u67E5": { skills: ["code-review"] }
176
+ }
177
+ };
178
+ var MINIMAL = {
179
+ stages: ["\u5F00\u53D1", "\u4EA4\u4ED8"],
180
+ start_stage: "\u5F00\u53D1",
181
+ transitions: [
182
+ { from: "\u5F00\u53D1", to: "\u4EA4\u4ED8", requires: ["todos_done"] }
183
+ ],
184
+ artifacts: [],
185
+ commit: {
186
+ policy: "task",
187
+ message_pattern: "",
188
+ message_hint: "\u65E0\u683C\u5F0F\u8981\u6C42",
189
+ checkpoints: ["\u4EA4\u4ED8"],
190
+ file_scope: false
191
+ },
192
+ high_risk_requires_verification: false,
193
+ stage_bindings: {
194
+ "\u5F00\u53D1": { skills: ["code-implement"] },
195
+ "\u4EA4\u4ED8": { skills: ["code-commit"], rules: ["commit-conventions"] }
196
+ }
197
+ };
198
+ function deriveCapabilities(config2) {
199
+ const caps = [];
200
+ const guards = new Set(config2.transitions.flatMap((t) => t.requires ?? []));
201
+ if (guards.has("verified")) caps.push("verification_gate");
202
+ if (guards.has("review_passed")) caps.push("review_gate");
203
+ if (guards.has("artifacts_present")) caps.push("artifact_gate");
204
+ if (config2.commit.file_scope) caps.push("file_scope");
205
+ return caps;
158
206
  }
159
-
160
- // Mirrors engine.checkFileScope.
161
- function checkFileScope(state, committing, config) {
162
- if (!config.commit.file_scope) return { ok: true, outside: [], noScope: false }
163
- const files = Array.isArray(state.files) ? state.files : []
164
- if (files.length === 0) return { ok: false, outside: [], noScope: true }
165
- const outside = committing.filter(file => !files.includes(file))
166
- return outside.length === 0
167
- ? { ok: true, outside: [], noScope: false }
168
- : { ok: false, outside, noScope: false }
207
+ function preset(id, version, label, description, config2) {
208
+ return { id, version, label, description, capabilities: deriveCapabilities(config2), config: config2 };
169
209
  }
170
-
171
- function stagedFiles() {
172
- try {
173
- return execSync('git diff --cached --name-only --diff-filter=ACMR', { encoding: 'utf8' })
174
- .split('\n')
175
- .map(line => line.trim())
176
- .filter(Boolean)
177
- } catch {
178
- return []
210
+ var FLOW_PRESETS = {
211
+ standard: preset("standard", 1, "\u6807\u51C6\u7814\u53D1", "\u9700\u6C42\u8BC4\u5BA1 \u2192 \u8BBE\u8BA1 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u4EE3\u7801\u5BA1\u6838\uFF0C\u542B\u4EA7\u7269\u95E8 + \u786E\u8BA4\u95E8", STANDARD),
212
+ agile: preset("agile", 1, "\u654F\u6377\u8F7B\u91CF", "\u9700\u6C42 \u2192 \u5F00\u53D1 \u2192 \u4EA4\u4ED8 \u2192 \u5BA1\u67E5\uFF0C\u56DB\u9636\u6BB5\u3001\u5C11\u4EA7\u7269", AGILE),
213
+ minimal: preset("minimal", 1, "\u7EAF\u4EE3\u7801", "\u5F00\u53D1 \u2192 \u4EA4\u4ED8\uFF0C\u4E24\u9636\u6BB5\uFF0C\u53EA\u7559\u63D0\u4EA4\u95E8\u7981", MINIMAL)
214
+ };
215
+ function dedupe(names) {
216
+ return [...new Set(names)];
217
+ }
218
+ function mergeBindings(base, override) {
219
+ if (override === void 0) return base;
220
+ let changed = false;
221
+ const merged = { ...base.stage_bindings };
222
+ for (const [stage, addition] of Object.entries(override)) {
223
+ const existing = merged[stage];
224
+ const skills = dedupe([...existing?.skills ?? [], ...addition.skills ?? []]);
225
+ const rules = dedupe([...existing?.rules ?? [], ...addition.rules ?? []]);
226
+ const binding = {};
227
+ if (skills.length > 0) binding.skills = skills;
228
+ if (rules.length > 0) binding.rules = rules;
229
+ merged[stage] = binding;
230
+ changed = true;
231
+ }
232
+ if (!changed) return base;
233
+ return { ...base, stage_bindings: merged };
234
+ }
235
+ function resolveFlow(flow, override) {
236
+ const preset2 = FLOW_PRESETS[flow];
237
+ if (preset2 === void 0) {
238
+ return { ok: false, code: "UNKNOWN_FLOW", flow, knownFlows: Object.keys(FLOW_PRESETS) };
179
239
  }
240
+ return { ok: true, config: mergeBindings(preset2.config, override?.stage_bindings), preset: preset2 };
180
241
  }
181
242
 
182
- // --- gather project state ---
183
- const cwd = process.cwd()
184
- const configPath = path.join(cwd, '.dsh', 'eng.json')
185
- let config
186
- if (fs.existsSync(configPath)) {
187
- let raw = ''
243
+ // src/hook.ts
244
+ var messageFile = process.argv[2];
245
+ function refuse(reason) {
246
+ console.error("");
247
+ console.error("dsh-task-engine \u63D0\u4EA4\u95E8\u7981\u62D2\u7EDD: " + reason);
248
+ console.error("\u8BF7\u5148\u7528 dev_task \u8D70\u5B8C\u524D\u7F6E\u9636\u6BB5\uFF08\u6216\u4FEE\u6B63\u63D0\u4EA4\u6D88\u606F\uFF09\uFF0C\u518D\u63D0\u4EA4\u3002");
249
+ console.error("");
250
+ process.exit(1);
251
+ }
252
+ function readJson(abs) {
188
253
  try {
189
- raw = fs.readFileSync(configPath, 'utf8')
254
+ return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
190
255
  } catch {
191
- raw = ''
256
+ return void 0;
192
257
  }
193
- let parsed
194
- try {
195
- parsed = JSON.parse(raw)
196
- } catch (error) {
197
- refuse('invalid JSON in .dsh/eng.json: ' + (error && error.message ? error.message : String(error)))
198
- }
199
- config = loadWorkflow(parsed)
200
- } else {
201
- config = loadWorkflow(undefined)
202
258
  }
203
-
259
+ function normalizeTask(task) {
260
+ return {
261
+ schema: 1,
262
+ id: String(task.id ?? ""),
263
+ title: String(task.title ?? ""),
264
+ branch: String(task.branch ?? ""),
265
+ work_size: task.work_size ?? "standard",
266
+ risk_level: task.risk_level ?? "standard",
267
+ stage: String(task.stage ?? ""),
268
+ requirement_confirmed: task.requirement_confirmed === true,
269
+ solution_confirmed: task.solution_confirmed === true,
270
+ items: Array.isArray(task.items) ? task.items : [],
271
+ verification: task.verification ?? { passed: false, evidence: [] },
272
+ review: task.review ?? { outcome: "pending" },
273
+ artifacts: task.artifacts ?? {},
274
+ files: Array.isArray(task.files) ? task.files : [],
275
+ commits: Array.isArray(task.commits) ? task.commits : [],
276
+ ...task.flow !== void 0 ? { flow: task.flow } : {}
277
+ };
278
+ }
204
279
  function loadTasks() {
205
- const dir = path.join(cwd, '.dsh')
206
- if (!fs.existsSync(dir)) return []
207
- const out = []
208
- const names = fs.readdirSync(dir).filter(name => name.startsWith('task-') && name.endsWith('.json'))
280
+ const dir = import_node_path.default.join(process.cwd(), ".dsh");
281
+ if (!import_node_fs.default.existsSync(dir)) return [];
282
+ const out = [];
283
+ const names = import_node_fs.default.readdirSync(dir).filter((name) => name.startsWith("task-") && name.endsWith(".json"));
209
284
  for (const name of names) {
210
- const task = readJson(path.join(dir, name))
211
- if (task && task.id) out.push({ file: path.join(dir, name), task })
285
+ const json = readJson(import_node_path.default.join(dir, name));
286
+ if (json !== void 0 && json.id !== void 0) out.push({ file: import_node_path.default.join(dir, name), task: normalizeTask(json) });
212
287
  }
213
- return out
288
+ return out;
214
289
  }
215
-
216
290
  function currentBranch() {
217
291
  try {
218
- return execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim()
292
+ return (0, import_node_child_process.execSync)("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
219
293
  } catch {
220
- return ''
294
+ return "";
295
+ }
296
+ }
297
+ function stagedEntries() {
298
+ try {
299
+ const raw = (0, import_node_child_process.execSync)("git -c core.quotePath=false diff --cached --name-status -z --diff-filter=ACMRDT", { encoding: "utf8" });
300
+ const parts = raw.split("\0");
301
+ const entries2 = [];
302
+ let i = 0;
303
+ while (i < parts.length) {
304
+ const status = parts[i];
305
+ if (status === void 0 || status === "") {
306
+ i++;
307
+ continue;
308
+ }
309
+ if (status.startsWith("R") || status.startsWith("C")) {
310
+ const from = parts[i + 1];
311
+ const to = parts[i + 2];
312
+ if (from !== void 0 && from !== "") entries2.push({ status, path: from });
313
+ if (to !== void 0 && to !== "") entries2.push({ status, path: to });
314
+ i += 3;
315
+ } else {
316
+ const path2 = parts[i + 1];
317
+ if (path2 !== void 0 && path2 !== "") entries2.push({ status, path: path2 });
318
+ i += 2;
319
+ }
320
+ }
321
+ return entries2;
322
+ } catch {
323
+ return [];
221
324
  }
222
325
  }
223
-
224
326
  function fileMtime(file) {
225
327
  try {
226
- return fs.statSync(file).mtimeMs
328
+ return import_node_fs.default.statSync(file).mtimeMs;
227
329
  } catch {
228
- return 0
330
+ return 0;
229
331
  }
230
332
  }
231
-
232
- function pickTask(tasks, branch) {
233
- if (tasks.length === 0) return undefined
234
- const byBranch = tasks.find(({ task }) => task.branch === branch)
235
- if (byBranch) return byBranch.task
236
- if (tasks.length === 1) return tasks[0].task
237
- const sorted = [...tasks].sort((a, b) => fileMtime(b.file) - fileMtime(a.file))
238
- return sorted[0].task
333
+ function pickTask(tasks2, branch) {
334
+ if (tasks2.length === 0) return void 0;
335
+ const byBranch = tasks2.find(({ task }) => task.branch === branch);
336
+ if (byBranch) return byBranch.task;
337
+ if (tasks2.length === 1) return tasks2[0].task;
338
+ const sorted = [...tasks2].sort((a, b) => fileMtime(b.file) - fileMtime(a.file));
339
+ return sorted[0].task;
340
+ }
341
+ function readCommitMessage(file) {
342
+ return import_node_fs.default.readFileSync(file, "utf8").split("\n").filter((line) => !line.startsWith("#")).join("\n").trim();
343
+ }
344
+ function extractTaskId(message2) {
345
+ const match = /^【([^【】\s]+)】/.exec(message2);
346
+ return match ? match[1] : void 0;
347
+ }
348
+ function defaultConfig() {
349
+ const resolved = resolveFlow("standard");
350
+ if (!resolved.ok) throw new Error('built-in "standard" preset is missing');
351
+ return resolved.config;
352
+ }
353
+ function loadWorkflow(parsed) {
354
+ if (parsed === void 0) return defaultConfig();
355
+ const flow = parsed.flow;
356
+ if (typeof flow !== "string" || flow.trim() === "") {
357
+ refuse('\u9879\u76EE .dsh/eng.json \u7F3A "flow" \u5B57\u6BB5\u2014\u2014\u8BBE\u4E3A standard|agile|minimal\uFF0C\u6216\u5220\u9664\u8BE5\u6587\u4EF6');
358
+ }
359
+ const override = typeof parsed.stage_bindings === "object" && parsed.stage_bindings !== null && !Array.isArray(parsed.stage_bindings) ? { stage_bindings: parsed.stage_bindings } : void 0;
360
+ const resolved = resolveFlow(flow, override);
361
+ if (!resolved.ok) {
362
+ refuse(`\u672A\u77E5\u6D41\u7A0B "${resolved.flow}"\uFF08\u5DF2\u77E5\u6D41\u7A0B\uFF1A${resolved.knownFlows.join("\u3001")}\uFF09`);
363
+ }
364
+ return resolved.config;
365
+ }
366
+ var cwd = process.cwd();
367
+ var configPath = import_node_path.default.join(cwd, ".dsh", "eng.json");
368
+ var config;
369
+ if (import_node_fs.default.existsSync(configPath)) {
370
+ let raw = "";
371
+ try {
372
+ raw = import_node_fs.default.readFileSync(configPath, "utf8");
373
+ } catch {
374
+ raw = "";
375
+ }
376
+ let parsed;
377
+ try {
378
+ parsed = JSON.parse(raw);
379
+ } catch (error) {
380
+ refuse("invalid JSON in .dsh/eng.json: " + (error instanceof Error ? error.message : String(error)));
381
+ }
382
+ config = loadWorkflow(parsed);
383
+ } else {
384
+ config = loadWorkflow(void 0);
239
385
  }
240
-
241
- // --- the gate ---
242
386
  if (!messageFile) {
243
- process.exit(0)
387
+ process.exit(0);
388
+ }
389
+ var message = readCommitMessage(messageFile);
390
+ var tasks = loadTasks();
391
+ var taskId = extractTaskId(message);
392
+ var state;
393
+ if (taskId !== void 0) {
394
+ const found = tasks.find(({ task }) => task.id === taskId);
395
+ if (!found) {
396
+ refuse(`\u63D0\u4EA4\u6D88\u606F\u91CC\u7684\u4EFB\u52A1 id "${taskId}" \u627E\u4E0D\u5230\u5BF9\u5E94\u7684\u4EFB\u52A1\u8BB0\u5F55\uFF08.dsh/task-<id>.json\uFF09`);
397
+ }
398
+ state = found.task;
399
+ } else {
400
+ state = pickTask(tasks, currentBranch());
244
401
  }
245
-
246
- const state = pickTask(loadTasks(), currentBranch())
247
402
  if (!state) {
248
- refuse('没有找到 dev_task 任务记录(.dsh/task-*.json)——请先 dev_task operation=create 建立任务')
403
+ refuse("\u6CA1\u6709\u627E\u5230 dev_task \u4EFB\u52A1\u8BB0\u5F55\uFF08.dsh/task-*.json\uFF09\u2014\u2014\u8BF7\u5148 dev_task operation=create \u5EFA\u7ACB\u4EFB\u52A1");
249
404
  }
250
-
251
- const checkpoint = commitCheckpoint(state, config)
405
+ if (state.flow !== void 0 && state.flow.config !== void 0) {
406
+ config = state.flow.config;
407
+ }
408
+ var verdict = validateCommitMessage(message, config);
409
+ if (!verdict.ok) {
410
+ refuse(verdict.errors?.[0] ?? "commit summary does not match the configured pattern");
411
+ }
412
+ var checkpoint = commitCheckpoint(state, config);
252
413
  if (!checkpoint.allowed) {
253
- refuse(checkpoint.reason + '(当前阶段: ' + state.stage + ')')
414
+ refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
254
415
  }
255
-
256
- const scope = checkFileScope(state, stagedFiles(), config)
416
+ var entries = stagedEntries();
417
+ var scope = checkFileScope(state, entries.map((entry) => entry.path), config);
257
418
  if (!scope.ok) {
258
419
  if (scope.noScope) {
259
- refuse('任务没有声明文件范围(files 为空)——先用 dev_task operation=scope 声明本任务涉及的文件')
420
+ refuse("\u4EFB\u52A1\u6CA1\u6709\u58F0\u660E\u6587\u4EF6\u8303\u56F4\uFF08files \u4E3A\u7A7A\uFF09\u2014\u2014\u5148\u7528 dev_task operation=scope \u58F0\u660E\u672C\u4EFB\u52A1\u6D89\u53CA\u7684\u6587\u4EF6");
260
421
  }
261
- refuse('提交了任务范围之外的文件: ' + scope.outside.join(', '))
422
+ const described = scope.outside.map((outside) => {
423
+ const entry = entries.find((candidate) => candidate.path === outside);
424
+ return entry === void 0 ? outside : `${entry.status} ${outside}`;
425
+ });
426
+ refuse("\u63D0\u4EA4\u4E86\u4EFB\u52A1\u8303\u56F4\u4E4B\u5916\u7684\u6587\u4EF6: " + described.join(", "));
262
427
  }
263
-
264
- const message = fs.readFileSync(messageFile, 'utf8')
265
- .split('\n')
266
- .filter(line => !line.startsWith('#'))
267
- .join('\n')
268
- .trim()
269
-
270
- const verdict = validateCommitMessage(message, config)
271
- if (!verdict.ok) {
272
- refuse(verdict.reason)
273
- }
274
-
275
- process.exit(0)
428
+ process.exit(0);