@dommaker/harness 1.0.0 → 1.1.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 +12 -13
- package/dist/cli/commands/sync-docs/agents-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.js +25 -4
- package/dist/cli/commands/sync-docs/agents-syncer.js.map +1 -1
- package/dist/completion-checkers/classify.d.ts +23 -0
- package/dist/completion-checkers/classify.d.ts.map +1 -0
- package/dist/completion-checkers/classify.js +37 -0
- package/dist/completion-checkers/classify.js.map +1 -0
- package/dist/completion-checkers/contract-presence.d.ts +12 -0
- package/dist/completion-checkers/contract-presence.d.ts.map +1 -0
- package/dist/completion-checkers/contract-presence.js +36 -0
- package/dist/completion-checkers/contract-presence.js.map +1 -0
- package/dist/completion-checkers/glob-match.d.ts +13 -0
- package/dist/completion-checkers/glob-match.d.ts.map +1 -0
- package/dist/completion-checkers/glob-match.js +58 -0
- package/dist/completion-checkers/glob-match.js.map +1 -0
- package/dist/completion-checkers/index.d.ts +19 -0
- package/dist/completion-checkers/index.d.ts.map +1 -0
- package/dist/completion-checkers/index.js +47 -0
- package/dist/completion-checkers/index.js.map +1 -0
- package/dist/completion-checkers/phase-format.d.ts +15 -0
- package/dist/completion-checkers/phase-format.d.ts.map +1 -0
- package/dist/completion-checkers/phase-format.js +42 -0
- package/dist/completion-checkers/phase-format.js.map +1 -0
- package/dist/completion-checkers/tdd-chain.d.ts +16 -0
- package/dist/completion-checkers/tdd-chain.d.ts.map +1 -0
- package/dist/completion-checkers/tdd-chain.js +57 -0
- package/dist/completion-checkers/tdd-chain.js.map +1 -0
- package/dist/completion-checkers/types.d.ts +77 -0
- package/dist/completion-checkers/types.d.ts.map +1 -0
- package/dist/completion-checkers/types.js +10 -0
- package/dist/completion-checkers/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pretool-use-hook.d.ts +16 -0
- package/dist/pretool-use-hook.d.ts.map +1 -0
- package/dist/pretool-use-hook.js +68 -0
- package/dist/pretool-use-hook.js.map +1 -0
- package/package.json +1 -1
- package/src/CONTEXT.md +1 -0
- package/src/__tests__/pretool-use-hook.test.ts +82 -0
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +55 -0
- package/src/cli/commands/sync-docs/agents-syncer.ts +26 -6
- package/src/completion-checkers/CONTEXT.md +25 -0
- package/src/completion-checkers/__tests__/contract-presence.test.ts +44 -0
- package/src/completion-checkers/__tests__/phase-format.test.ts +60 -0
- package/src/completion-checkers/__tests__/tdd-chain.test.ts +99 -0
- package/src/completion-checkers/classify.ts +47 -0
- package/src/completion-checkers/contract-presence.ts +40 -0
- package/src/completion-checkers/glob-match.ts +54 -0
- package/src/completion-checkers/index.ts +19 -0
- package/src/completion-checkers/phase-format.ts +48 -0
- package/src/completion-checkers/tdd-chain.ts +63 -0
- package/src/completion-checkers/types.ts +84 -0
- package/src/index.ts +7 -0
- package/src/pretool-use-hook.ts +68 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion Checkers 类型定义(T7-E1,studio#160)
|
|
3
|
+
*
|
|
4
|
+
* 三个纯判定函数共享的输入/输出/配置类型。
|
|
5
|
+
* 与 ConstraintCheck 闭环注册表无关:直接 export,不注册、不碰 checkers/index.ts。
|
|
6
|
+
* 函数不碰文件系统与 git,commits 由调用方(studio 侧 git log)供给。
|
|
7
|
+
*/
|
|
8
|
+
/** WU 提交输入(有序,base..HEAD 升序) */
|
|
9
|
+
export interface CommitInput {
|
|
10
|
+
/** 完整 sha */
|
|
11
|
+
sha: string;
|
|
12
|
+
/** commit subject(第一行) */
|
|
13
|
+
subject: string;
|
|
14
|
+
/** commit body(trailer 所在) */
|
|
15
|
+
body: string;
|
|
16
|
+
/** 本 commit 触碰的文件清单(相对路径) */
|
|
17
|
+
files: string[];
|
|
18
|
+
/**
|
|
19
|
+
* 是否 merge commit。调用方有 git 数据时应显式供给(如 %P 父数 > 1);
|
|
20
|
+
* 缺省时按 subject 启发式判定(`/^Merge\s/`)。
|
|
21
|
+
*/
|
|
22
|
+
isMerge?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** 判定结论:pass / violation / waiver(豁免放行,记台账)/ skip(不适用,不记台账) */
|
|
25
|
+
export type CheckerVerdict = 'pass' | 'violation' | 'waiver' | 'skip';
|
|
26
|
+
/** 单 commit 判定明细 */
|
|
27
|
+
export interface CommitVerdict {
|
|
28
|
+
sha: string;
|
|
29
|
+
verdict: CheckerVerdict;
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
32
|
+
/** verifyTddChain 结果。整体 verdict 只取 pass/violation/skip;waiver 是 commit 级结论 */
|
|
33
|
+
export interface TddChainResult {
|
|
34
|
+
checker: 'tdd-chain';
|
|
35
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
36
|
+
commits: CommitVerdict[];
|
|
37
|
+
}
|
|
38
|
+
/** verifyPhaseFormat 结果 */
|
|
39
|
+
export interface PhaseFormatResult {
|
|
40
|
+
checker: 'phase-format';
|
|
41
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
42
|
+
commits: CommitVerdict[];
|
|
43
|
+
}
|
|
44
|
+
/** verifyContractPresence 上下文(类型→字段的判定方法映射是代码不是配置) */
|
|
45
|
+
export interface ContractPresenceContext {
|
|
46
|
+
/** review 类型契约:studio agent-loop 已解析的 metadata.reviewReport */
|
|
47
|
+
reviewReport?: unknown;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
/** verifyContractPresence 结果 */
|
|
51
|
+
export interface ContractPresenceResult {
|
|
52
|
+
checker: 'contract-presence';
|
|
53
|
+
verdict: 'pass' | 'violation' | 'skip';
|
|
54
|
+
detail?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* completion_checkers 配置(对应 yml 顶层键 `completion_checkers:`,studio 侧自解)。
|
|
58
|
+
* 只配开关/glob/契约类型清单;协议格式(Tested-By、Tests: none、phase 结构)写死为机制本体。
|
|
59
|
+
* 缺省 = 全开 + 默认 glob。
|
|
60
|
+
*/
|
|
61
|
+
export interface CompletionCheckersConfig {
|
|
62
|
+
/** 总开关,缺省 true */
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
/** 各 checker 开关,缺省全开 */
|
|
65
|
+
checkers?: {
|
|
66
|
+
tddChain?: boolean;
|
|
67
|
+
phaseFormat?: boolean;
|
|
68
|
+
contractPresence?: boolean;
|
|
69
|
+
};
|
|
70
|
+
/** 测试文件 glob,缺省 DEFAULT_TEST_GLOBS */
|
|
71
|
+
testGlobs?: string[];
|
|
72
|
+
/** 非代码文件 glob,缺省 DEFAULT_NONCODE_GLOBS */
|
|
73
|
+
noncodeGlobs?: string[];
|
|
74
|
+
/** 契约类型清单:类型在清单内才判定,无表项 = skip */
|
|
75
|
+
contracts?: string[];
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/completion-checkers/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,gCAAgC;AAChC,MAAM,WAAW,WAAW;IAC1B,aAAa;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,oBAAoB;AACpB,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,2BAA2B;AAC3B,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IACvC,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,sDAAsD;AACtD,MAAM,WAAW,uBAAuB;IACtC,+DAA+D;IAC/D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,gCAAgC;AAChC,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,mBAAmB,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IACvC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,wBAAwB;IACvC,kBAAkB;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wBAAwB;IACxB,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,0CAA0C;IAC1C,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Completion Checkers 类型定义(T7-E1,studio#160)
|
|
4
|
+
*
|
|
5
|
+
* 三个纯判定函数共享的输入/输出/配置类型。
|
|
6
|
+
* 与 ConstraintCheck 闭环注册表无关:直接 export,不注册、不碰 checkers/index.ts。
|
|
7
|
+
* 函数不碰文件系统与 git,commits 由调用方(studio 侧 git log)供给。
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/completion-checkers/types.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,cAAc,SAAS,CAAC;AAKxB,cAAc,QAAQ,CAAC;AAKvB,cAAc,SAAS,CAAC;AAKxB,cAAc,cAAc,CAAC;AAK7B,cAAc,WAAW,CAAC;AAK1B,cAAc,2BAA2B,CAAC;AAK1C,cAAc,WAAW,CAAC;AAK1B,cAAc,aAAa,CAAC;AAK5B,cAAc,UAAU,CAAC;AAKzB,cAAc,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,cAAc,SAAS,CAAC;AAKxB,cAAc,QAAQ,CAAC;AAKvB,cAAc,SAAS,CAAC;AAKxB,cAAc,cAAc,CAAC;AAK7B,cAAc,WAAW,CAAC;AAK1B,cAAc,2BAA2B,CAAC;AAK1C,cAAc,WAAW,CAAC;AAK1B,cAAc,aAAa,CAAC;AAK5B,cAAc,UAAU,CAAC;AAKzB,cAAc,gBAAgB,CAAC;AAO/B,cAAc,uBAAuB,CAAC;AAKtC,cAAc,aAAa,CAAC;AAK5B,cAAc,SAAS,CAAC;AAKxB,cAAc,SAAS,CAAC;AAKxB,cAAc,UAAU,CAAC;AAKzB,cAAc,WAAW,CAAC;AAK1B,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,YAAY,EAAE,iCAAiC,EAAE,MAAM,0CAA0C,CAAC;AAQlG,OAAO,KAAK,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEtG,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAKlG,8CAA8C;AAC9C,eAAO,MAAM,WAAW,wCAAwB,CAAC;AAEjD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAE7B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,aAAa,EAC5B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,oBAAoB,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAAE,GACtF,OAAO,CAAC,qBAAqB,CAAC,CAOhC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,IAAI,CAAC,CAEf"}
|
package/dist/index.js
CHANGED
|
@@ -78,6 +78,12 @@ __exportStar(require("./safety"), exports);
|
|
|
78
78
|
// ========================================
|
|
79
79
|
__exportStar(require("./verification"), exports);
|
|
80
80
|
// ========================================
|
|
81
|
+
// Completion Checkers 导出(T7-E1,studio#160)
|
|
82
|
+
// WU 收尾软观测三纯判定函数:tdd-chain / phase-format / contract-presence。
|
|
83
|
+
// 纯函数直接 export,不进 ConstraintCheck 闭环注册表。
|
|
84
|
+
// ========================================
|
|
85
|
+
__exportStar(require("./completion-checkers"), exports);
|
|
86
|
+
// ========================================
|
|
81
87
|
// Dashboard 数据导出
|
|
82
88
|
// ========================================
|
|
83
89
|
__exportStar(require("./dashboard"), exports);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** hook 标识:与 studio-agent 生成版同一 marker(配置幂等检测共用口径) */
|
|
2
|
+
export declare const HOOK_MARKER = "harness-command-gate";
|
|
3
|
+
/**
|
|
4
|
+
* 判定一次 PreToolUse 是否放行。
|
|
5
|
+
* block 级命中 → allowed=false;warn/audit/干净命令/坏输入一律 allowed=true。
|
|
6
|
+
*/
|
|
7
|
+
export declare function decidePreToolUse(rawStdin: string): {
|
|
8
|
+
allowed: boolean;
|
|
9
|
+
command: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* 执行一次 hook:返回进程 exit code(2 = 阻断,0 = 放行)。
|
|
13
|
+
* 阻断时原因写 stderr(provider 把 stderr 作为阻断原因回填模型)。
|
|
14
|
+
*/
|
|
15
|
+
export declare function runPreToolUseHook(rawStdin: string): number;
|
|
16
|
+
//# sourceMappingURL=pretool-use-hook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretool-use-hook.d.ts","sourceRoot":"","sources":["../src/pretool-use-hook.ts"],"names":[],"mappings":"AAmBA,sDAAsD;AACtD,eAAO,MAAM,WAAW,yBAAyB,CAAC;AAMlD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAexF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HOOK_MARKER = void 0;
|
|
4
|
+
exports.decidePreToolUse = decidePreToolUse;
|
|
5
|
+
exports.runPreToolUseHook = runPreToolUseHook;
|
|
6
|
+
/**
|
|
7
|
+
* PreToolUse 执法脚本 — command-gate hook 固化版(studio#153)
|
|
8
|
+
*
|
|
9
|
+
* 内容 = studio-agent provider-hooks.buildHookScriptContent 生成脚本(#147)的固化版,
|
|
10
|
+
* 归属归位:谁的东西谁发货——CommandGate 是 harness 的,hook 脚本随 harness 包出厂。
|
|
11
|
+
* 编译产物 = 包内 dist/pretool-use-hook.js,provider hook 配置(codex hooks.json /
|
|
12
|
+
* kimi config.toml)直接指向 require.resolve('@dommaker/harness') 同目录的该文件,
|
|
13
|
+
* 不再由 studio-agent 按 worktree 生成、不再内嵌绝对路径。
|
|
14
|
+
*
|
|
15
|
+
* 语义(与生成版一致):
|
|
16
|
+
* - stdin 收 provider PreToolUse JSON(tool_input.command);
|
|
17
|
+
* - CommandGate.isAllowed 判定 block 级黑名单,命中 → stderr 写原因 + exit 2
|
|
18
|
+
* (codex/kimi 阻断语义:exit 2 + stderr 阻断,其余 exit code fail-open);
|
|
19
|
+
* - fail-open:stdin 非 JSON / 缺字段 / CommandGate 异常一律放行——宁可漏拦
|
|
20
|
+
* 也不全体 Bash 秒断,拦截层只是纵深防御的一道;
|
|
21
|
+
* - warn/audit 级命中同样放行(isAllowed 只看 block 级),与生成版行为一致。
|
|
22
|
+
*/
|
|
23
|
+
const command_1 = require("./gates/command");
|
|
24
|
+
/** hook 标识:与 studio-agent 生成版同一 marker(配置幂等检测共用口径) */
|
|
25
|
+
exports.HOOK_MARKER = 'harness-command-gate';
|
|
26
|
+
/**
|
|
27
|
+
* 判定一次 PreToolUse 是否放行。
|
|
28
|
+
* block 级命中 → allowed=false;warn/audit/干净命令/坏输入一律 allowed=true。
|
|
29
|
+
*/
|
|
30
|
+
function decidePreToolUse(rawStdin) {
|
|
31
|
+
let input = {};
|
|
32
|
+
try {
|
|
33
|
+
input = JSON.parse(rawStdin || '{}');
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// 非 JSON stdin:放行
|
|
37
|
+
}
|
|
38
|
+
const command = (input.tool_input && input.tool_input.command) || '';
|
|
39
|
+
try {
|
|
40
|
+
const gate = new command_1.CommandGate();
|
|
41
|
+
return { allowed: gate.isAllowed(command), command };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// CommandGate 加载/判定异常:fail-open
|
|
45
|
+
return { allowed: true, command };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 执行一次 hook:返回进程 exit code(2 = 阻断,0 = 放行)。
|
|
50
|
+
* 阻断时原因写 stderr(provider 把 stderr 作为阻断原因回填模型)。
|
|
51
|
+
*/
|
|
52
|
+
function runPreToolUseHook(rawStdin) {
|
|
53
|
+
const { allowed, command } = decidePreToolUse(rawStdin);
|
|
54
|
+
if (!allowed) {
|
|
55
|
+
console.error(`[${exports.HOOK_MARKER}] blocked: ${command}`);
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
/* istanbul ignore next -- 进程入口仅做 stdin 收集 + exit,逻辑由 runPreToolUseHook 单测覆盖 */
|
|
61
|
+
if (require.main === module) {
|
|
62
|
+
let raw = '';
|
|
63
|
+
process.stdin.on('data', (c) => { raw += c; });
|
|
64
|
+
process.stdin.on('end', () => {
|
|
65
|
+
process.exit(runPreToolUseHook(raw));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=pretool-use-hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pretool-use-hook.js","sourceRoot":"","sources":["../src/pretool-use-hook.ts"],"names":[],"mappings":";;;AA8BA,4CAeC;AAMD,8CAOC;AA1DD;;;;;;;;;;;;;;;;GAgBG;AACH,6CAA8C;AAE9C,sDAAsD;AACzC,QAAA,WAAW,GAAG,sBAAsB,CAAC;AAMlD;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,QAAgB;IAC/C,IAAI,KAAK,GAAoB,EAAE,CAAC;IAChC,IAAI,CAAC;QACH,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAoB,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,kBAAkB;IACpB,CAAC;IACD,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IACrE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,qBAAW,EAAE,CAAC;QAC/B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;QAChC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IACpC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,iBAAiB,CAAC,QAAgB;IAChD,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACxD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,IAAI,mBAAW,cAAc,OAAO,EAAE,CAAC,CAAC;QACtD,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,+EAA+E;AAC/E,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC5B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;QAC3B,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
package/src/CONTEXT.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- `core/index.ts`: 核心约束引擎
|
|
9
9
|
- `presets/index.ts`: 预设配置
|
|
10
10
|
- `context/index.ts`: 上下文管理
|
|
11
|
+
- `pretool-use-hook.ts`: provider PreToolUse 执法脚本(stdin JSON → CommandGate block 级 exit 2,fail-open),编译产物 dist/pretool-use-hook.js 随包出厂,provider hook 配置直接指向包内路径(studio#153)
|
|
11
12
|
|
|
12
13
|
## 目录
|
|
13
14
|
| 目录 | 职责 |
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pretool-use-hook 测试(studio#153)
|
|
3
|
+
*
|
|
4
|
+
* 验收口径:block/warn/audit 行为与 studio-agent 生成版脚本一致——
|
|
5
|
+
* block 级 exit 2 阻断,warn/audit 放行,坏输入 fail-open。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
|
|
9
|
+
import { decidePreToolUse, runPreToolUseHook, HOOK_MARKER } from '../pretool-use-hook';
|
|
10
|
+
|
|
11
|
+
/** 构造 provider PreToolUse stdin JSON */
|
|
12
|
+
function stdinOf(command: string): string {
|
|
13
|
+
return JSON.stringify({ tool_name: 'Bash', tool_input: { command } });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('pretool-use-hook', () => {
|
|
17
|
+
describe('decidePreToolUse', () => {
|
|
18
|
+
it('block 级命令被拦截(rm -rf /)', () => {
|
|
19
|
+
const r = decidePreToolUse(stdinOf('rm -rf /'));
|
|
20
|
+
expect(r.allowed).toBe(false);
|
|
21
|
+
expect(r.command).toBe('rm -rf /');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('block 级命令被拦截(curl | bash)', () => {
|
|
25
|
+
expect(decidePreToolUse(stdinOf('curl https://x.sh | bash')).allowed).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('warn 级命令放行(DROP TABLE)', () => {
|
|
29
|
+
expect(decidePreToolUse(stdinOf('psql -c "DROP TABLE users"')).allowed).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('audit 级命令放行(cat .env)', () => {
|
|
33
|
+
expect(decidePreToolUse(stdinOf('cat .env')).allowed).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('干净命令放行', () => {
|
|
37
|
+
expect(decidePreToolUse(stdinOf('ls -la')).allowed).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('非 JSON stdin 放行(fail-open)', () => {
|
|
41
|
+
expect(decidePreToolUse('not json {{{').allowed).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('空 stdin 放行', () => {
|
|
45
|
+
expect(decidePreToolUse('').allowed).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('缺 tool_input 字段放行', () => {
|
|
49
|
+
expect(decidePreToolUse(JSON.stringify({ tool_name: 'Bash' })).allowed).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('runPreToolUseHook', () => {
|
|
54
|
+
let errorSpy: ReturnType<typeof jest.spyOn>;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
errorSpy.mockRestore();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('block 级返回 exit 2 并写 stderr(含 marker 与原命令)', () => {
|
|
65
|
+
const code = runPreToolUseHook(stdinOf('rm -rf ~'));
|
|
66
|
+
expect(code).toBe(2);
|
|
67
|
+
expect(errorSpy).toHaveBeenCalledWith(`[${HOOK_MARKER}] blocked: rm -rf ~`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('放行返回 exit 0 且不写 stderr', () => {
|
|
71
|
+
const code = runPreToolUseHook(stdinOf('git status'));
|
|
72
|
+
expect(code).toBe(0);
|
|
73
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('warn/audit 级返回 exit 0(与生成版一致:只拦 block)', () => {
|
|
77
|
+
expect(runPreToolUseHook(stdinOf('killall node'))).toBe(0);
|
|
78
|
+
expect(runPreToolUseHook(stdinOf('cat ~/.ssh/id_rsa'))).toBe(0);
|
|
79
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -242,6 +242,61 @@ describe('sync-docs --agents', () => {
|
|
|
242
242
|
fs.rmSync(testDir, { recursive: true, force: true });
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
+
describe('知识入口行:.studio/CONTEXT.md 正本模型(studio #188)', () => {
|
|
246
|
+
it('.studio/CONTEXT.md 存在时生成正本行,替代散置指引(散置文件同时在场也一样),--check 无漂移', async () => {
|
|
247
|
+
const testDir = path.join(tempDir, 'studio-context-model');
|
|
248
|
+
createFixture(testDir, { pnpm: true, withCapabilities: true });
|
|
249
|
+
// 夹具已带散置 src/CONTEXT.md:正本在场时整行替代,与是否残留散置文件无关
|
|
250
|
+
fs.mkdirSync(path.join(testDir, '.studio'), { recursive: true });
|
|
251
|
+
fs.writeFileSync(path.join(testDir, '.studio', 'CONTEXT.md'), '# 模块上下文\n\n## src\n\n职责:夹具\n');
|
|
252
|
+
|
|
253
|
+
await syncDocs({ projectPath: testDir, agents: true });
|
|
254
|
+
|
|
255
|
+
const content = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
|
|
256
|
+
expect(content).toContain('模块上下文正本:`.studio/CONTEXT.md`(模块锚点组织),改动代码时同步更新');
|
|
257
|
+
expect(content).not.toContain('各源码目录的 `CONTEXT.md`');
|
|
258
|
+
expect(content).not.toContain('缺失目录可由 `harness sync-docs` 生成模板');
|
|
259
|
+
// 知识库行不受影响
|
|
260
|
+
expect(content).toContain('项目知识库,用 `harness knowledge` 查询');
|
|
261
|
+
|
|
262
|
+
// 幂等 + --check 通过(正本模型下无漂移)
|
|
263
|
+
const second = await syncDocs({ projectPath: testDir, agents: true });
|
|
264
|
+
expect(second).toBe(true);
|
|
265
|
+
expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(true);
|
|
266
|
+
|
|
267
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('.studio/CONTEXT.md 不存在时保持散置模型行,--check 无漂移', async () => {
|
|
271
|
+
const testDir = path.join(tempDir, 'scattered-context-model');
|
|
272
|
+
createFixture(testDir, { pnpm: true, withCapabilities: true });
|
|
273
|
+
|
|
274
|
+
await syncDocs({ projectPath: testDir, agents: true });
|
|
275
|
+
|
|
276
|
+
const content = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
|
|
277
|
+
expect(content).toContain('各源码目录的 `CONTEXT.md` 是权威模块文档(现有 1 个),改动代码时同步更新');
|
|
278
|
+
expect(content).not.toContain('模块上下文正本');
|
|
279
|
+
|
|
280
|
+
expect(await syncDocs({ projectPath: testDir, agents: true, check: true })).toBe(true);
|
|
281
|
+
|
|
282
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('.studio/CONTEXT.md 是目录而非文件时不视为正本', async () => {
|
|
286
|
+
const testDir = path.join(tempDir, 'studio-context-dir');
|
|
287
|
+
createFixture(testDir, { pnpm: true, withCapabilities: true });
|
|
288
|
+
fs.mkdirSync(path.join(testDir, '.studio', 'CONTEXT.md'), { recursive: true });
|
|
289
|
+
|
|
290
|
+
await syncDocs({ projectPath: testDir, agents: true });
|
|
291
|
+
|
|
292
|
+
const content = fs.readFileSync(path.join(testDir, 'AGENTS.md'), 'utf-8');
|
|
293
|
+
expect(content).toContain('各源码目录的 `CONTEXT.md` 是权威模块文档');
|
|
294
|
+
expect(content).not.toContain('模块上下文正本');
|
|
295
|
+
|
|
296
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
245
300
|
it('空项目应该生成兜底内容(无 package.json、无治理配置、无知识库)', async () => {
|
|
246
301
|
const testDir = path.join(tempDir, 'empty-project');
|
|
247
302
|
// 只放应被跳过的目录:依赖/构建产物/隐藏目录
|
|
@@ -60,10 +60,11 @@ export interface GovernanceInfo {
|
|
|
60
60
|
*/
|
|
61
61
|
export async function buildAgentsMd(projectPath: string, srcDirs: string[]): Promise<string> {
|
|
62
62
|
const pkg = await readPackageJsonLite(projectPath);
|
|
63
|
-
const [dirs, governance, hasKnowledge, contextDocCount] = await Promise.all([
|
|
63
|
+
const [dirs, governance, hasKnowledge, hasStudioContext, contextDocCount] = await Promise.all([
|
|
64
64
|
getTopLevelDirEntries(projectPath, srcDirs),
|
|
65
65
|
getGovernanceInfo(projectPath),
|
|
66
66
|
hasKnowledgeDir(projectPath),
|
|
67
|
+
hasStudioContextDoc(projectPath),
|
|
67
68
|
countContextDocs(projectPath, srcDirs),
|
|
68
69
|
]);
|
|
69
70
|
|
|
@@ -127,11 +128,17 @@ export async function buildAgentsMd(projectPath: string, srcDirs: string[]): Pro
|
|
|
127
128
|
// 写进受 --check 漂移比对的文档会制造「必然过期」竞态(任何知识写入都使已提交文档过期)
|
|
128
129
|
lines.push('- `.harness/knowledge/`:项目知识库,用 `harness knowledge` 查询');
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
if (hasStudioContext) {
|
|
132
|
+
// 正本模型(studio #152/T12):模块上下文沉淀归并到业务仓 .studio/CONTEXT.md,
|
|
133
|
+
// 散置 CONTEXT.md 指引与该裁决矛盾,正本在场时整行替代
|
|
134
|
+
lines.push('- 模块上下文正本:`.studio/CONTEXT.md`(模块锚点组织),改动代码时同步更新');
|
|
135
|
+
} else {
|
|
136
|
+
lines.push(
|
|
137
|
+
contextDocCount > 0
|
|
138
|
+
? `- 各源码目录的 \`CONTEXT.md\` 是权威模块文档(现有 ${contextDocCount} 个),改动代码时同步更新`
|
|
139
|
+
: '- 各源码目录的 `CONTEXT.md` 是权威模块文档,缺失目录可由 `harness sync-docs` 生成模板'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
135
142
|
lines.push('');
|
|
136
143
|
|
|
137
144
|
return lines.join('\n');
|
|
@@ -247,6 +254,19 @@ async function hasKnowledgeDir(projectPath: string): Promise<boolean> {
|
|
|
247
254
|
}
|
|
248
255
|
}
|
|
249
256
|
|
|
257
|
+
/**
|
|
258
|
+
* 判断 .studio/CONTEXT.md 是否存在(正本模型标记,studio #152/T12):
|
|
259
|
+
* 模块上下文沉淀归并到业务仓单一正本(模块锚点组织)时,知识入口行改指正本,
|
|
260
|
+
* 不再输出散置 CONTEXT.md 指引。只判存在性,不解析内容。
|
|
261
|
+
*/
|
|
262
|
+
async function hasStudioContextDoc(projectPath: string): Promise<boolean> {
|
|
263
|
+
try {
|
|
264
|
+
return (await fs.stat(path.join(projectPath, '.studio', 'CONTEXT.md'))).isFile();
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
250
270
|
/** 统计源码根内的 CONTEXT.md 数量(含源码根本身,跨根去重) */
|
|
251
271
|
async function countContextDocs(projectPath: string, srcDirs: string[]): Promise<number> {
|
|
252
272
|
const found = new Set<string>();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# completion-checkers/
|
|
2
|
+
|
|
3
|
+
## 职责
|
|
4
|
+
WU 收尾软观测三纯判定函数(T7-E1,studio#160):tdd-chain 引用链 / phase-format subject 结构 / contract-presence 契约在场。纯函数,不碰文件系统与 git,commits 由调用方(studio completion-gates 第四段守卫,T7-E2)供给。
|
|
5
|
+
|
|
6
|
+
## 核心导出
|
|
7
|
+
- `verifyTddChain(commits, config)`(`tdd-chain.ts`)——实现 commit 须带 trailer `Tested-By: <sha>`(`TESTED_BY_RE` 写死);被引 sha 须在本提交集内、位置在前、文件清单命中 test_globs;`Tests: none`(`TESTS_NONE_RE`)→ waiver;纯测试/纯非代码 commit 天然免检
|
|
8
|
+
- `verifyPhaseFormat(commits, config)`(`phase-format.ts`)——非 merge commit subject 命中 `PHASE_SUBJECT_RE`(`^phase\([a-z0-9-]+\):\s+\S`,写死);merge commit 记违规(`CommitInput.isMerge` 优先,缺省 subject 启发式)
|
|
9
|
+
- `verifyContractPresence(type, context, config)`(`contract-presence.ts`)——yml contracts 清单查表,无表项 = skip;类型→判定方法映射是代码不是配置(review → `context.reviewReport` 在场);清单内无判定方法 = violation(配置与代码失配)
|
|
10
|
+
- `CompletionCheckersConfig`(`types.ts`)——enabled 总开关 / checkers 各开关 / testGlobs / noncodeGlobs / contracts;协议格式不进配置
|
|
11
|
+
- `classifyCommitFiles` / `resolveGlobs`(`classify.ts`)——文件分类,tdd-chain 与 phase-format 共享口径
|
|
12
|
+
- `matchGlob` / `matchAnyGlob` / `DEFAULT_TEST_GLOBS` / `DEFAULT_NONCODE_GLOBS`(`glob-match.ts`)——轻量 glob 匹配(`**`/`*`/`?`)
|
|
13
|
+
|
|
14
|
+
## 依赖关系
|
|
15
|
+
- 无内部依赖(纯函数模块,自包含)
|
|
16
|
+
|
|
17
|
+
## 约定
|
|
18
|
+
- 与 ConstraintCheck 闭环注册表无关:直接 export,禁止注册进 checkers/index.ts
|
|
19
|
+
- verdict 四态:pass / violation / waiver(豁免放行,commit 级)/ skip(不适用,不记台账)
|
|
20
|
+
- 新增契约类型 = `CONTRACT_JUDGMENTS` 加一条映射 + 测试
|
|
21
|
+
- 协议格式(Tested-By、Tests: none、phase 结构)写死为机制本体,不得配置化
|
|
22
|
+
|
|
23
|
+
## 注意事项
|
|
24
|
+
- commits 输入有序(base..HEAD 升序),位置判定比索引不比时间戳
|
|
25
|
+
- Tested-By 引用支持 7-40 位 sha 前缀匹配
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyContractPresence 测试(studio#160 验收:契约标记缺失 / 无表项 skip)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifyContractPresence } from '../contract-presence';
|
|
6
|
+
|
|
7
|
+
describe('verifyContractPresence', () => {
|
|
8
|
+
it('无表项类型 → skip(不算违规)', () => {
|
|
9
|
+
const result = verifyContractPresence('review', {}, { contracts: [] });
|
|
10
|
+
expect(result.verdict).toBe('skip');
|
|
11
|
+
expect(result.detail).toContain('无 contracts 表项');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('contracts 缺省(未配置)→ skip', () => {
|
|
15
|
+
expect(verifyContractPresence('review', { reviewReport: {} }).verdict).toBe('skip');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('review 在场(reviewReport 已解析)→ pass', () => {
|
|
19
|
+
const result = verifyContractPresence(
|
|
20
|
+
'review',
|
|
21
|
+
{ reviewReport: { verdict: 'pass' } },
|
|
22
|
+
{ contracts: ['review'] },
|
|
23
|
+
);
|
|
24
|
+
expect(result.verdict).toBe('pass');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each([undefined, null])('review 契约标记缺失(reviewReport=%s)→ violation', (reviewReport) => {
|
|
28
|
+
const result = verifyContractPresence('review', { reviewReport }, { contracts: ['review'] });
|
|
29
|
+
expect(result.verdict).toBe('violation');
|
|
30
|
+
expect(result.detail).toContain('契约标记缺失');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('类型在清单内但无判定方法注册 → violation(配置与代码失配)', () => {
|
|
34
|
+
const result = verifyContractPresence('unknown-type', {}, { contracts: ['unknown-type'] });
|
|
35
|
+
expect(result.verdict).toBe('violation');
|
|
36
|
+
expect(result.detail).toContain('无判定方法注册');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('checker 关闭 → skip', () => {
|
|
40
|
+
expect(
|
|
41
|
+
verifyContractPresence('review', {}, { contracts: ['review'], checkers: { contractPresence: false } }).verdict,
|
|
42
|
+
).toBe('skip');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verifyPhaseFormat 测试(studio#160 验收:subject 不合规 / merge commit 违规)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { verifyPhaseFormat } from '../phase-format';
|
|
6
|
+
import type { CommitInput } from '../types';
|
|
7
|
+
|
|
8
|
+
const sha = (n: number) => n.toString(16).padStart(40, '0');
|
|
9
|
+
|
|
10
|
+
function commit(partial: Partial<CommitInput> & { sha: string }): CommitInput {
|
|
11
|
+
return { subject: 'phase(impl): x', body: '', files: ['src/a.ts'], ...partial };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('verifyPhaseFormat', () => {
|
|
15
|
+
it('合规 subject → pass', () => {
|
|
16
|
+
const commits = [
|
|
17
|
+
commit({ sha: sha(1), subject: 'phase(test): add chain tests' }),
|
|
18
|
+
commit({ sha: sha(2), subject: 'phase(impl-2): wire it' }),
|
|
19
|
+
];
|
|
20
|
+
const result = verifyPhaseFormat(commits);
|
|
21
|
+
expect(result.verdict).toBe('pass');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it.each([
|
|
25
|
+
'feat: not a phase subject',
|
|
26
|
+
'phase: missing scope',
|
|
27
|
+
'phase(Impl): uppercase scope',
|
|
28
|
+
'phase(impl):missing space after colon',
|
|
29
|
+
'phase(impl): ',
|
|
30
|
+
])('subject 不合规 → violation:%s', (subject) => {
|
|
31
|
+
const result = verifyPhaseFormat([commit({ sha: sha(1), subject })]);
|
|
32
|
+
expect(result.verdict).toBe('violation');
|
|
33
|
+
expect(result.commits[0].reason).toContain('subject 不合规');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('merge commit(显式 isMerge)→ violation', () => {
|
|
37
|
+
const result = verifyPhaseFormat([commit({ sha: sha(1), isMerge: true })]);
|
|
38
|
+
expect(result.verdict).toBe('violation');
|
|
39
|
+
expect(result.commits[0].reason).toContain('merge commit');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('merge commit(isMerge 缺省时 subject 启发式)→ violation', () => {
|
|
43
|
+
const result = verifyPhaseFormat([
|
|
44
|
+
commit({ sha: sha(1), subject: "Merge branch 'w/x'" }),
|
|
45
|
+
]);
|
|
46
|
+
expect(result.verdict).toBe('violation');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('显式 isMerge=false 覆盖 subject 启发式', () => {
|
|
50
|
+
const result = verifyPhaseFormat([
|
|
51
|
+
commit({ sha: sha(1), subject: 'Merge 字样但非 merge', isMerge: false }),
|
|
52
|
+
]);
|
|
53
|
+
// subject 不含 phase 结构,仍因 subject 违规——但不应记 merge 违规
|
|
54
|
+
expect(result.commits[0].reason).not.toContain('merge commit');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('checker 关闭 → skip', () => {
|
|
58
|
+
expect(verifyPhaseFormat([commit({ sha: sha(1) })], { checkers: { phaseFormat: false } }).verdict).toBe('skip');
|
|
59
|
+
});
|
|
60
|
+
});
|