@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/.p0-test.mjs +292 -0
- package/README.md +8 -5
- package/docs/manual.html +26 -16
- package/hooks/commit-msg +377 -224
- package/lib/client.js +12 -9
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +2 -0
- package/lib/controller.js +11 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +52 -5
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +28 -5
- package/lib/engine.js +37 -7
- package/lib/engine.js.map +1 -1
- package/lib/hook.d.ts +12 -0
- package/lib/hook.js +258 -0
- package/lib/hook.js.map +1 -0
- package/lib/workflows.js +2 -2
- package/lib/workflows.js.map +1 -1
- package/package.json +4 -3
- package/skills/code-verify/SKILL.md +1 -1
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
case
|
|
109
|
-
return
|
|
110
|
-
case
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
135
|
-
const ready =
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return { allowed: true, label:
|
|
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 ===
|
|
142
|
-
const done =
|
|
143
|
-
if (done > 0)
|
|
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:
|
|
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
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
254
|
+
return JSON.parse(import_node_fs.default.readFileSync(abs, "utf8"));
|
|
190
255
|
} catch {
|
|
191
|
-
|
|
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 =
|
|
206
|
-
if (!
|
|
207
|
-
const out = []
|
|
208
|
-
const names =
|
|
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
|
|
211
|
-
if (
|
|
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(
|
|
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
|
|
328
|
+
return import_node_fs.default.statSync(file).mtimeMs;
|
|
227
329
|
} catch {
|
|
228
|
-
return 0
|
|
330
|
+
return 0;
|
|
229
331
|
}
|
|
230
332
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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(
|
|
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
|
-
|
|
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 +
|
|
414
|
+
refuse((checkpoint.reason ?? "commit checkpoint rejected") + "\uFF08\u5F53\u524D\u9636\u6BB5: " + state.stage + "\uFF09");
|
|
254
415
|
}
|
|
255
|
-
|
|
256
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|