@dommaker/harness 1.11.0 → 1.12.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 +20 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts +48 -0
- package/dist/cli/commands/constraints-pack-proposal.d.ts.map +1 -0
- package/dist/cli/commands/constraints-pack-proposal.js +169 -0
- package/dist/cli/commands/constraints-pack-proposal.js.map +1 -0
- package/dist/cli/commands/constraints-reactivate.d.ts +55 -0
- package/dist/cli/commands/constraints-reactivate.d.ts.map +1 -0
- package/dist/cli/commands/constraints-reactivate.js +175 -0
- package/dist/cli/commands/constraints-reactivate.js.map +1 -0
- package/dist/cli/commands/constraints-report.d.ts +12 -1
- package/dist/cli/commands/constraints-report.d.ts.map +1 -1
- package/dist/cli/commands/constraints-report.js +35 -2
- package/dist/cli/commands/constraints-report.js.map +1 -1
- package/dist/cli/commands/constraints-retire.d.ts +49 -4
- package/dist/cli/commands/constraints-retire.d.ts.map +1 -1
- package/dist/cli/commands/constraints-retire.js +116 -34
- package/dist/cli/commands/constraints-retire.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts.map +1 -1
- package/dist/cli/commands/definitions.js +31 -1
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/knowledge-view.d.ts +6 -3
- package/dist/cli/commands/knowledge-view.d.ts.map +1 -1
- package/dist/cli/commands/knowledge-view.js +8 -26
- package/dist/cli/commands/knowledge-view.js.map +1 -1
- package/dist/cli/commands/report.d.ts.map +1 -1
- package/dist/cli/commands/report.js +9 -5
- package/dist/cli/commands/report.js.map +1 -1
- package/dist/cli/state-io.d.ts +8 -1
- package/dist/cli/state-io.d.ts.map +1 -1
- package/dist/cli/state-io.js +2 -1
- package/dist/cli/state-io.js.map +1 -1
- package/dist/core/app-constraints-loader.d.ts +28 -0
- package/dist/core/app-constraints-loader.d.ts.map +1 -0
- package/dist/core/app-constraints-loader.js +187 -0
- package/dist/core/app-constraints-loader.js.map +1 -0
- package/dist/core/constraints/checker.js +1 -1
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/checkers/index.d.ts +20 -9
- package/dist/core/constraints/checkers/index.d.ts.map +1 -1
- package/dist/core/constraints/checkers/index.js +40 -11
- package/dist/core/constraints/checkers/index.js.map +1 -1
- package/dist/core/constraints/checkers/templated/file-exists.d.ts +16 -0
- package/dist/core/constraints/checkers/templated/file-exists.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/file-exists.js +108 -0
- package/dist/core/constraints/checkers/templated/file-exists.js.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts +10 -0
- package/dist/core/constraints/checkers/templated/glob.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/glob.js +51 -0
- package/dist/core/constraints/checkers/templated/glob.js.map +1 -0
- package/dist/core/constraints/checkers/templated/index.d.ts +12 -0
- package/dist/core/constraints/checkers/templated/index.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/index.js +17 -0
- package/dist/core/constraints/checkers/templated/index.js.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts +15 -0
- package/dist/core/constraints/checkers/templated/regex-scan.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js +129 -0
- package/dist/core/constraints/checkers/templated/regex-scan.js.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts +17 -0
- package/dist/core/constraints/checkers/templates-registry.d.ts.map +1 -0
- package/dist/core/constraints/checkers/templates-registry.js +23 -0
- package/dist/core/constraints/checkers/templates-registry.js.map +1 -0
- package/dist/core/constraints/checkers/types.d.ts +14 -0
- package/dist/core/constraints/checkers/types.d.ts.map +1 -1
- package/dist/core/constraints/checkers/types.js.map +1 -1
- package/dist/core/constraints/definitions/guidelines.d.ts.map +1 -1
- package/dist/core/constraints/definitions/guidelines.js +0 -14
- package/dist/core/constraints/definitions/guidelines.js.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.d.ts.map +1 -1
- package/dist/core/constraints/definitions/iron-laws.js +15 -0
- package/dist/core/constraints/definitions/iron-laws.js.map +1 -1
- package/dist/core/constraints/usage-report.d.ts +58 -6
- package/dist/core/constraints/usage-report.d.ts.map +1 -1
- package/dist/core/constraints/usage-report.js +61 -12
- package/dist/core/constraints/usage-report.js.map +1 -1
- package/dist/core/project-config-loader.d.ts +9 -6
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +18 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +50 -0
- package/dist/gates/command.js.map +1 -1
- package/dist/knowledge/lifecycle.d.ts +14 -0
- package/dist/knowledge/lifecycle.d.ts.map +1 -1
- package/dist/knowledge/lifecycle.js +50 -0
- package/dist/knowledge/lifecycle.js.map +1 -1
- package/dist/presets/standard.d.ts +1 -1
- package/dist/presets/standard.d.ts.map +1 -1
- package/dist/presets/standard.js +2 -3
- package/dist/presets/standard.js.map +1 -1
- package/dist/types/constraint.d.ts +24 -0
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/app-constraints-loader.test.ts +220 -0
- package/src/__tests__/iron-laws.test.ts +5 -5
- package/src/cli/commands/CONTEXT.md +3 -2
- package/src/cli/commands/__tests__/check.test.ts +22 -4
- package/src/cli/commands/__tests__/constraints-pack-proposal.test.ts +142 -0
- package/src/cli/commands/__tests__/constraints-reactivate.test.ts +210 -0
- package/src/cli/commands/__tests__/constraints-report.test.ts +175 -6
- package/src/cli/commands/__tests__/constraints-retire.test.ts +135 -10
- package/src/cli/commands/__tests__/knowledge.test.ts +6 -16
- package/src/cli/commands/__tests__/report-app-constraints.test.ts +67 -0
- package/src/cli/commands/constraints-pack-proposal.ts +172 -0
- package/src/cli/commands/constraints-reactivate.ts +197 -0
- package/src/cli/commands/constraints-report.ts +44 -2
- package/src/cli/commands/constraints-retire.ts +129 -36
- package/src/cli/commands/definitions.ts +31 -1
- package/src/cli/commands/knowledge-view.ts +9 -28
- package/src/cli/commands/report.ts +9 -5
- package/src/cli/state-io.ts +8 -1
- package/src/core/CONTEXT.md +5 -4
- package/src/core/app-constraints-loader.ts +165 -0
- package/src/core/constraints/__tests__/collect-constraints.test.ts +3 -1
- package/src/core/constraints/__tests__/usage-report.test.ts +63 -0
- package/src/core/constraints/checker.ts +1 -1
- package/src/core/constraints/checkers/__tests__/templated-checkers.test.ts +180 -0
- package/src/core/constraints/checkers/__tests__/templates-registry.test.ts +81 -0
- package/src/core/constraints/checkers/index.ts +44 -10
- package/src/core/constraints/checkers/templated/file-exists.ts +75 -0
- package/src/core/constraints/checkers/templated/glob.ts +46 -0
- package/src/core/constraints/checkers/templated/index.ts +12 -0
- package/src/core/constraints/checkers/templated/regex-scan.ts +132 -0
- package/src/core/constraints/checkers/templates-registry.ts +22 -0
- package/src/core/constraints/checkers/types.ts +15 -0
- package/src/core/constraints/definitions/guidelines.ts +0 -15
- package/src/core/constraints/definitions/iron-laws.ts +16 -0
- package/src/core/constraints/usage-report.ts +128 -13
- package/src/core/project-config-loader.ts +19 -8
- package/src/gates/__tests__/command.test.ts +54 -0
- package/src/gates/command.ts +51 -0
- package/src/knowledge/CONTEXT.md +3 -1
- package/src/knowledge/__tests__/lifecycle.test.ts +81 -0
- package/src/knowledge/lifecycle.ts +49 -0
- package/src/presets/__tests__/standard.test.ts +5 -4
- package/src/presets/standard.ts +2 -3
- package/src/types/constraint.ts +28 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 两层注册表闭环校验扩展(ADR-0033 块 3 子项 1)测试
|
|
3
|
+
*
|
|
4
|
+
* - getConstraintCheck:字符串 id / 内置约束走内置注册表(现状不动);
|
|
5
|
+
* source='app' 按模板 id + params 实例化;模板 id 未注册 → 抛错(闭环保留)
|
|
6
|
+
* - 内置对账闭环不动:每条 kind=check 内置约束有 checker、无孤儿实现
|
|
7
|
+
* (既有断言在 src/__tests__/iron-laws.test.ts,此处只补应用层维度)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, afterEach } from '@jest/globals';
|
|
11
|
+
import {
|
|
12
|
+
getConstraintCheck,
|
|
13
|
+
TEMPLATES,
|
|
14
|
+
type ConstraintCheck,
|
|
15
|
+
type TemplatedCheckerFactory,
|
|
16
|
+
} from '../index';
|
|
17
|
+
import type { Constraint } from '../../../../types/constraint';
|
|
18
|
+
|
|
19
|
+
const FAKE_PARAMS = { p: 1 };
|
|
20
|
+
|
|
21
|
+
const fakeFactory: TemplatedCheckerFactory = {
|
|
22
|
+
validateParams: () => [],
|
|
23
|
+
create: (id, params) => ({
|
|
24
|
+
id,
|
|
25
|
+
evaluate: () => ({ pass: params === FAKE_PARAMS || params.p === 1 }),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function appConstraint(overrides: Partial<Constraint> = {}): Constraint {
|
|
30
|
+
return {
|
|
31
|
+
id: 'app_fake',
|
|
32
|
+
kind: 'check',
|
|
33
|
+
rule: 'r',
|
|
34
|
+
message: 'm',
|
|
35
|
+
severity: 'warning',
|
|
36
|
+
trigger: 'commit',
|
|
37
|
+
enforcement: '',
|
|
38
|
+
source: 'app',
|
|
39
|
+
checker: 'fake-template',
|
|
40
|
+
params: FAKE_PARAMS,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
TEMPLATES.delete('fake-template');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getConstraintCheck 两层分发', () => {
|
|
50
|
+
it('字符串 id 走内置注册表(现状口径:未注册返回 undefined)', () => {
|
|
51
|
+
expect(getConstraintCheck('capability_sync')).toBeDefined();
|
|
52
|
+
expect(getConstraintCheck('removed_legacy')).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('内置约束对象按 id 查找(source 缺省 = builtin)', () => {
|
|
56
|
+
const builtin: Constraint = {
|
|
57
|
+
id: 'capability_sync',
|
|
58
|
+
kind: 'check',
|
|
59
|
+
rule: 'r',
|
|
60
|
+
message: 'm',
|
|
61
|
+
severity: 'warning',
|
|
62
|
+
trigger: 'module_modification',
|
|
63
|
+
enforcement: 'update-capabilities',
|
|
64
|
+
};
|
|
65
|
+
expect(getConstraintCheck(builtin)?.id).toBe('capability_sync');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('source=app:按模板 id + params 实例化 checker', () => {
|
|
69
|
+
TEMPLATES.set('fake-template', fakeFactory);
|
|
70
|
+
const check = getConstraintCheck(appConstraint()) as ConstraintCheck;
|
|
71
|
+
expect(check.id).toBe('app_fake');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('source=app 模板未注册 → 抛错(闭环保留,不许静默 pass)', () => {
|
|
75
|
+
expect(() => getConstraintCheck(appConstraint())).toThrow(/未注册/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('source=app 缺 checker 字段 → 抛错', () => {
|
|
79
|
+
expect(() => getConstraintCheck(appConstraint({ checker: undefined }))).toThrow(/闭环校验失败/);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 约束检查器注册表(工单 21
|
|
2
|
+
* 约束检查器注册表(工单 21;ADR-0033 拆两层)
|
|
3
3
|
*
|
|
4
|
-
* checker.ts 编排层通过 getConstraintCheck(
|
|
4
|
+
* checker.ts 编排层通过 getConstraintCheck() 查找实现。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* 两层注册表:
|
|
7
|
+
* - 内置 checker(registry,按约束 id):模块加载时校验全部 kind='check' 的内置约束
|
|
8
|
+
* 都有已注册 checker,且注册表中没有无对应 check 定义的孤儿实现;
|
|
9
|
+
* 任一不满足即抛错(加载期失败,不许静默 pass,ADR-0001)。
|
|
10
|
+
* - 模板 checker(TEMPLATES,按模板 id):填空式参数化模板,供应用层约束
|
|
11
|
+
* (source='app',`.harness/constraints.yml`)按 checker + params 实例化。
|
|
12
|
+
* 闭环语义扩展而非开口:应用层约束在加载期(app-constraints-loader)校验
|
|
13
|
+
* 模板 id 存在 + validateParams 通过;getConstraintCheck 对未注册模板 id 抛错。
|
|
9
14
|
*/
|
|
10
15
|
|
|
16
|
+
import type { Constraint } from '../../../types/constraint';
|
|
11
17
|
import type { ConstraintCheck } from './types';
|
|
12
18
|
import { getAllConstraints } from '../definitions';
|
|
13
19
|
|
|
@@ -20,14 +26,15 @@ import { contextDocSync } from './context-doc-sync';
|
|
|
20
26
|
import { docsFreshness } from './docs-freshness';
|
|
21
27
|
import { noHardcodedCredentials } from './no-hardcoded-credentials';
|
|
22
28
|
import { governancePresence } from './governance-presence';
|
|
29
|
+
import { TEMPLATES } from './templates-registry';
|
|
23
30
|
|
|
24
31
|
const CHECKS: ConstraintCheck[] = [
|
|
25
32
|
// severity='error'
|
|
26
33
|
noCompletionWithoutVerification,
|
|
27
34
|
noTestSimplification,
|
|
28
35
|
docsFreshness,
|
|
29
|
-
// severity='warning'
|
|
30
36
|
noHardcodedCredentials,
|
|
37
|
+
// severity='warning'
|
|
31
38
|
capabilitySync,
|
|
32
39
|
contextDocSync,
|
|
33
40
|
governancePresence,
|
|
@@ -35,6 +42,14 @@ const CHECKS: ConstraintCheck[] = [
|
|
|
35
42
|
|
|
36
43
|
const registry = new Map<string, ConstraintCheck>(CHECKS.map(c => [c.id, c]));
|
|
37
44
|
|
|
45
|
+
// ========================================
|
|
46
|
+
// 模板注册表(ADR-0033,应用层约束用)
|
|
47
|
+
// ========================================
|
|
48
|
+
|
|
49
|
+
// TEMPLATES 住在 ./templates-registry(独立模块防值级循环,见该文件头注释);
|
|
50
|
+
// 此处 re-export 供编排层与测试单口取数
|
|
51
|
+
export { TEMPLATES } from './templates-registry';
|
|
52
|
+
|
|
38
53
|
// ========================================
|
|
39
54
|
// 注册表闭环校验(加载期)
|
|
40
55
|
// ========================================
|
|
@@ -60,11 +75,29 @@ for (const id of registry.keys()) {
|
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
78
|
+
* 查找检查实现
|
|
79
|
+
*
|
|
80
|
+
* - 传字符串 id 或内置约束:按 id 查内置注册表,未注册返回 undefined
|
|
81
|
+
* (编排层对 kind='check' 未注册的情况抛错)
|
|
82
|
+
* - 传 source='app' 的约束:按 checker 模板 id + params 实例化;
|
|
83
|
+
* 模板 id 未注册 → 抛错(闭环保留,不许静默 pass)
|
|
65
84
|
*/
|
|
66
|
-
export function getConstraintCheck(
|
|
67
|
-
|
|
85
|
+
export function getConstraintCheck(idOrConstraint: string | Constraint): ConstraintCheck | undefined {
|
|
86
|
+
if (typeof idOrConstraint === 'string') {
|
|
87
|
+
return registry.get(idOrConstraint);
|
|
88
|
+
}
|
|
89
|
+
if (idOrConstraint.source === 'app') {
|
|
90
|
+
const templateId = idOrConstraint.checker;
|
|
91
|
+
const factory = templateId ? TEMPLATES.get(templateId) : undefined;
|
|
92
|
+
if (!factory) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`[harness] 约束注册表闭环校验失败:应用层约束 "${idOrConstraint.id}" 引用的 checker 模板 ` +
|
|
95
|
+
`"${templateId ?? '(未填写)'}" 未注册。请在 checkers/templated/ 中实现并注册。`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return factory.create(idOrConstraint.id, idOrConstraint.params ?? {});
|
|
99
|
+
}
|
|
100
|
+
return registry.get(idOrConstraint.id);
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
/**
|
|
@@ -77,6 +110,7 @@ export function registeredCheckCount(): number {
|
|
|
77
110
|
export { buildCheckEnv, normalizeCheckOutcome } from './types';
|
|
78
111
|
export type {
|
|
79
112
|
ConstraintCheck,
|
|
113
|
+
TemplatedCheckerFactory,
|
|
80
114
|
CheckEnv,
|
|
81
115
|
CheckOutcome,
|
|
82
116
|
CheckDetail,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* file-exists 模板:文件存在性约束(ADR-0033,块 3 子项 2)
|
|
3
|
+
*
|
|
4
|
+
* 形态参照 context-doc-sync:存在性判定 + 证据行(fail 必须可归因,harness#119)。
|
|
5
|
+
* 与 context-doc-sync 的差别:目标由参数显式给出(加载期已校验),
|
|
6
|
+
* 无「约定未采用」三态,故永不 skip。
|
|
7
|
+
*
|
|
8
|
+
* 参数:
|
|
9
|
+
* - path(可选):相对项目根的单文件路径
|
|
10
|
+
* - glob(可选):文件集 glob(fast-glob 口径,dot: true 以覆盖 .harness 等点目录)
|
|
11
|
+
* - mustExist(可选,缺省 true):true = 必须存在;false = 必须不存在
|
|
12
|
+
* path 与 glob 至少填一个;都填 = 两者都要满足。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'fs';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import * as glob from 'fast-glob';
|
|
18
|
+
import { formatEvidence, type ConstraintCheck, type TemplatedCheckerFactory } from '../types';
|
|
19
|
+
|
|
20
|
+
export const fileExists: TemplatedCheckerFactory = {
|
|
21
|
+
validateParams(params) {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
if (params.path !== undefined && typeof params.path !== 'string') {
|
|
24
|
+
errors.push('path 必须是字符串');
|
|
25
|
+
}
|
|
26
|
+
if (params.glob !== undefined && typeof params.glob !== 'string') {
|
|
27
|
+
errors.push('glob 必须是字符串');
|
|
28
|
+
}
|
|
29
|
+
const hasPath = typeof params.path === 'string' && params.path.length > 0;
|
|
30
|
+
const hasGlob = typeof params.glob === 'string' && params.glob.length > 0;
|
|
31
|
+
if (!hasPath && !hasGlob) {
|
|
32
|
+
errors.push('path 或 glob 至少填一个');
|
|
33
|
+
}
|
|
34
|
+
if (params.mustExist !== undefined && typeof params.mustExist !== 'boolean') {
|
|
35
|
+
errors.push('mustExist 必须是布尔值');
|
|
36
|
+
}
|
|
37
|
+
return errors;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(id, params): ConstraintCheck {
|
|
41
|
+
const rel = params.path as string | undefined;
|
|
42
|
+
const globPattern = params.glob as string | undefined;
|
|
43
|
+
const mustExist = params.mustExist !== false;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id,
|
|
47
|
+
evaluate(env) {
|
|
48
|
+
const pathHit = rel !== undefined && existsSync(join(env.projectPath, rel));
|
|
49
|
+
const globHits = globPattern !== undefined
|
|
50
|
+
? glob.sync(globPattern, { cwd: env.projectPath, dot: true, onlyFiles: false })
|
|
51
|
+
: [];
|
|
52
|
+
|
|
53
|
+
if (mustExist) {
|
|
54
|
+
const missing: string[] = [];
|
|
55
|
+
if (rel !== undefined && !pathHit) missing.push(rel);
|
|
56
|
+
if (globPattern !== undefined && globHits.length === 0) missing.push(`${globPattern}(无匹配文件)`);
|
|
57
|
+
if (missing.length > 0) {
|
|
58
|
+
return { pass: false, evidence: formatEvidence('必需文件不存在', missing) };
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// mustExist=false:任何目标存在即违规
|
|
64
|
+
const present: string[] = [
|
|
65
|
+
...(pathHit ? [rel!] : []),
|
|
66
|
+
...globHits,
|
|
67
|
+
];
|
|
68
|
+
if (present.length > 0) {
|
|
69
|
+
return { pass: false, evidence: formatEvidence('禁存文件存在', present) };
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模板侧轻量 glob 匹配(支持 `**`、`*`、`?`,纯字符串判定,不碰文件系统)
|
|
3
|
+
*
|
|
4
|
+
* 与 completion-checkers/glob-match 同一匹配语义;此处另写一份是因为分层
|
|
5
|
+
* (harness#88 参数化守卫)不许 core 值导入同层领域模块——约束域的模板
|
|
6
|
+
* 参数匹配不住 completion-checkers 的 commits 分类模块。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function escapeRegExp(s: string): string {
|
|
10
|
+
return s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** glob → 锚定正则。前缀 `**` + `/` 匹配零或多段路径;`*` 不跨 `/`;`?` 匹配单个非 `/` 字符 */
|
|
14
|
+
function globToRegExp(glob: string): RegExp {
|
|
15
|
+
let re = '';
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < glob.length) {
|
|
18
|
+
const c = glob[i];
|
|
19
|
+
if (c === '*') {
|
|
20
|
+
if (glob[i + 1] === '*') {
|
|
21
|
+
if (glob[i + 2] === '/') {
|
|
22
|
+
re += '(?:[^/]+/)*';
|
|
23
|
+
i += 3;
|
|
24
|
+
} else {
|
|
25
|
+
re += '.*';
|
|
26
|
+
i += 2;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
re += '[^/]*';
|
|
30
|
+
i += 1;
|
|
31
|
+
}
|
|
32
|
+
} else if (c === '?') {
|
|
33
|
+
re += '[^/]';
|
|
34
|
+
i += 1;
|
|
35
|
+
} else {
|
|
36
|
+
re += escapeRegExp(c);
|
|
37
|
+
i += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return new RegExp('^' + re + '$');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 单 glob 匹配(相对路径正斜杠口径) */
|
|
44
|
+
export function matchGlob(file: string, glob: string): boolean {
|
|
45
|
+
return globToRegExp(glob).test(file);
|
|
46
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 填空式 checker 模板(ADR-0033 两层约束模型,块 3 子项 2)
|
|
3
|
+
*
|
|
4
|
+
* 每个模板 = TemplatedCheckerFactory(validateParams + create),注册进
|
|
5
|
+
* checkers/index.ts 的 TEMPLATES 供应用层约束(`.harness/constraints.yml`)
|
|
6
|
+
* 按 checker + params 实例化。
|
|
7
|
+
*
|
|
8
|
+
* 模板只认参数不认应用语义(harness 零业务逻辑):约束条文、文案归应用仓。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { regexScan } from './regex-scan';
|
|
12
|
+
export { fileExists } from './file-exists';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* regex-scan 模板:正则扫描约束(ADR-0033,块 3 子项 2)
|
|
3
|
+
*
|
|
4
|
+
* 形态参照 no-hardcoded-credentials:scope='diff'(缺省)扫 staged diff 新增行,
|
|
5
|
+
* diff 为空/不可用时退化为变更文件内容扫描;scope='content' 直接扫变更文件内容。
|
|
6
|
+
* 命中即违规,证据行点名文件与命中行(harness#119:fail 必须可归因)。
|
|
7
|
+
*
|
|
8
|
+
* 参数:
|
|
9
|
+
* - pattern(必填):正则字符串
|
|
10
|
+
* - glob(可选):限定文件集(相对路径,支持 `**`/`*`/`?`)
|
|
11
|
+
* - scope(可选):'diff' | 'content',缺省 'diff'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'fs';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { formatEvidence, type CheckDetail, type CheckEnv, type ConstraintCheck, type TemplatedCheckerFactory } from '../types';
|
|
17
|
+
import { matchGlob } from './glob';
|
|
18
|
+
|
|
19
|
+
/** 证据行单行长度上限(命中行可能是整行配置/长文本) */
|
|
20
|
+
const MAX_LINE = 120;
|
|
21
|
+
|
|
22
|
+
interface AddedLine {
|
|
23
|
+
file: string;
|
|
24
|
+
line: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 从 unified diff 中取新增行(按 +++ b/<path> 归属文件,glob 过滤文件集) */
|
|
28
|
+
function collectAddedLines(diff: string, inScope: (file: string) => boolean): AddedLine[] {
|
|
29
|
+
const out: AddedLine[] = [];
|
|
30
|
+
let file = '';
|
|
31
|
+
for (const raw of diff.split('\n')) {
|
|
32
|
+
if (raw.startsWith('+++ b/')) {
|
|
33
|
+
file = raw.slice('+++ b/'.length);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (raw.startsWith('+++') || raw.startsWith('diff --git')) {
|
|
37
|
+
if (raw.startsWith('diff --git')) file = '';
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (raw.startsWith('+') && file && inScope(file)) {
|
|
41
|
+
out.push({ file, line: raw.slice(1) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hitEvidence(pattern: RegExp, hits: string[]): CheckDetail {
|
|
48
|
+
return {
|
|
49
|
+
pass: false,
|
|
50
|
+
evidence: formatEvidence(`命中禁用模式 /${pattern.source}/`, hits),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clip(line: string): string {
|
|
55
|
+
const t = line.trim();
|
|
56
|
+
return t.length > MAX_LINE ? `${t.slice(0, MAX_LINE)}…` : t;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const regexScan: TemplatedCheckerFactory = {
|
|
60
|
+
validateParams(params) {
|
|
61
|
+
const errors: string[] = [];
|
|
62
|
+
const pattern = params.pattern;
|
|
63
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
64
|
+
errors.push('pattern 必填(正则字符串)');
|
|
65
|
+
} else {
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(pattern);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
errors.push(`pattern 不是合法正则:${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (params.glob !== undefined && typeof params.glob !== 'string') {
|
|
73
|
+
errors.push('glob 必须是字符串');
|
|
74
|
+
}
|
|
75
|
+
if (params.scope !== undefined && params.scope !== 'diff' && params.scope !== 'content') {
|
|
76
|
+
errors.push("scope 必须是 'diff' 或 'content'");
|
|
77
|
+
}
|
|
78
|
+
return errors;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
create(id, params): ConstraintCheck {
|
|
82
|
+
const pattern = new RegExp(params.pattern as string);
|
|
83
|
+
const glob = params.glob as string | undefined;
|
|
84
|
+
const scope = (params.scope as 'diff' | 'content' | undefined) ?? 'diff';
|
|
85
|
+
const inScope = (file: string) => !glob || matchGlob(file, glob);
|
|
86
|
+
|
|
87
|
+
const scanChangedFiles = (env: CheckEnv): string[] => {
|
|
88
|
+
const hits: string[] = [];
|
|
89
|
+
for (const file of (env.context.changedFiles ?? []).filter(inScope)) {
|
|
90
|
+
let content: string;
|
|
91
|
+
try {
|
|
92
|
+
content = readFileSync(join(env.projectPath, file), 'utf-8');
|
|
93
|
+
} catch {
|
|
94
|
+
continue; // 文件不可读(已删除/二进制)→ 跳过
|
|
95
|
+
}
|
|
96
|
+
for (const line of content.split('\n')) {
|
|
97
|
+
if (pattern.test(line)) hits.push(`${file}: ${clip(line)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return hits;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
async evaluate(env) {
|
|
106
|
+
if (scope === 'content') {
|
|
107
|
+
const hits = scanChangedFiles(env);
|
|
108
|
+
return hits.length > 0 ? hitEvidence(pattern, hits) : true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// scope='diff':staged diff 新增行;diff 为空/不可用 → 退化变更文件内容扫描
|
|
112
|
+
let scanned = false;
|
|
113
|
+
try {
|
|
114
|
+
const diff = await env.stagedDiff();
|
|
115
|
+
const added = collectAddedLines(diff, inScope);
|
|
116
|
+
if (added.length > 0) {
|
|
117
|
+
scanned = true;
|
|
118
|
+
const hits = added.filter(a => pattern.test(a.line)).map(a => `${a.file}: ${clip(a.line)}`);
|
|
119
|
+
if (hits.length > 0) return hitEvidence(pattern, hits);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// diff 不可用 → 退化为文件内容扫描
|
|
123
|
+
}
|
|
124
|
+
if (!scanned) {
|
|
125
|
+
const hits = scanChangedFiles(env);
|
|
126
|
+
if (hits.length > 0) return hitEvidence(pattern, hits);
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 填空式 checker 模板注册表(ADR-0033 两层约束模型)
|
|
3
|
+
*
|
|
4
|
+
* 独立成模块而非住在 checkers/index.ts:应用层加载器(core/app-constraints-loader)
|
|
5
|
+
* 要做加载期模板校验,若直接引 checkers/index.ts 会经 project-config-loader →
|
|
6
|
+
* 本注册表 → 内置 checker → project-config-loader 形成值级循环(checker 初始化时
|
|
7
|
+
* 拿到 undefined)。本模块不引内置 checker,无环。
|
|
8
|
+
*
|
|
9
|
+
* 闭环语义:模板 id 未注册即不可用——加载期由 app-constraints-loader 校验,
|
|
10
|
+
* 运行期由 checkers/index.ts 的 getConstraintCheck 抛错兜底。
|
|
11
|
+
*
|
|
12
|
+
* 首批模板:regex-scan(正则扫描)/ file-exists(文件存在性),块 3 子项 2。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { TemplatedCheckerFactory } from './types';
|
|
16
|
+
import { regexScan, fileExists } from './templated';
|
|
17
|
+
|
|
18
|
+
/** 模板注册表(模板 id → 工厂);测试可注册替身模板,用后须注销 */
|
|
19
|
+
export const TEMPLATES = new Map<string, TemplatedCheckerFactory>([
|
|
20
|
+
['regex-scan', regexScan],
|
|
21
|
+
['file-exists', fileExists],
|
|
22
|
+
]);
|
|
@@ -148,6 +148,21 @@ export interface ConstraintCheck {
|
|
|
148
148
|
evaluate(env: CheckEnv): Promise<CheckOutcome> | CheckOutcome;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* 填空式 checker 模板工厂(ADR-0033 两层约束模型)
|
|
153
|
+
*
|
|
154
|
+
* 每个模板 = validateParams + create:参数坏在加载期早报(validateParams),
|
|
155
|
+
* create 只在参数已校验后调用。注册点 = checkers/index.ts 的 TEMPLATES。
|
|
156
|
+
* 类型住在 types.ts(而非 index.ts)是为了模板实现文件只依赖本模块,
|
|
157
|
+
* 不与注册表形成值级循环导入。
|
|
158
|
+
*/
|
|
159
|
+
export interface TemplatedCheckerFactory {
|
|
160
|
+
/** 参数校验:返回错误清单,空数组 = 通过(加载期跑,参数坏早报) */
|
|
161
|
+
validateParams(params: Record<string, unknown>): string[];
|
|
162
|
+
/** 按约束 id + 已校验参数实例化 checker */
|
|
163
|
+
create(id: string, params: Record<string, unknown>): ConstraintCheck;
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
/**
|
|
152
167
|
* 构造纯上下文标志检查(无 I/O 的轻量约束)
|
|
153
168
|
*/
|
|
@@ -10,21 +10,6 @@
|
|
|
10
10
|
import type { Constraint } from '../../../types/constraint';
|
|
11
11
|
|
|
12
12
|
export const WARNING_CONSTRAINTS: Record<string, Constraint> = {
|
|
13
|
-
/**
|
|
14
|
-
* 禁止硬编码凭证
|
|
15
|
-
* 原因:密码/token/密钥泄露是最严重的安全漏洞,一旦提交到版本控制即不可逆
|
|
16
|
-
*/
|
|
17
|
-
no_hardcoded_credentials: {
|
|
18
|
-
id: 'no_hardcoded_credentials',
|
|
19
|
-
kind: 'check',
|
|
20
|
-
rule: 'NO HARDCODED PASSWORDS, TOKENS, SECRETS, OR CREDENTIALS IN ANY SOURCE FILE',
|
|
21
|
-
message: '禁止在代码/文档/配置文件中硬编码密码、token、密钥、API key',
|
|
22
|
-
severity: 'warning',
|
|
23
|
-
trigger: ['code_implementation', 'doc_update', 'config_change', 'commit'],
|
|
24
|
-
enforcement: 'credential-scan',
|
|
25
|
-
description: `凭证(密码/token/密钥/API key)必须从环境变量或加密配置读取,严禁以明文形式出现在任何源代码文件中。包括但不限于:源码、Markdown 文档、YAML/JSON 配置、脚本文件。模板文件(.example/.sample/.template)允许占位符但不得包含真实值。建议配合 pre-commit hook 做自动扫描。`,
|
|
26
|
-
},
|
|
27
|
-
|
|
28
13
|
/**
|
|
29
14
|
* 功能清单同步
|
|
30
15
|
* 例外:内部重构、bug fix
|
|
@@ -25,6 +25,22 @@ export const ERROR_CONSTRAINTS: Record<string, Constraint> = {
|
|
|
25
25
|
description: '在声明任何任务完成之前,必须运行新鲜的验证命令(项目声明的测试 + type check),以新鲜输出作为完成证据。验证命令集由项目自行声明,全量验证留给 CI 流程。',
|
|
26
26
|
},
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 禁止硬编码凭证
|
|
30
|
+
* 原因:密码/token/密钥泄露是最严重的安全漏洞,一旦提交到版本控制即不可逆。
|
|
31
|
+
* ADR-0032:安全底线类约束,warning 不阻断等于没有——升级为 error(违规即阻断)。
|
|
32
|
+
*/
|
|
33
|
+
no_hardcoded_credentials: {
|
|
34
|
+
id: 'no_hardcoded_credentials',
|
|
35
|
+
kind: 'check',
|
|
36
|
+
rule: 'NO HARDCODED PASSWORDS, TOKENS, SECRETS, OR CREDENTIALS IN ANY SOURCE FILE',
|
|
37
|
+
message: '禁止在代码/文档/配置文件中硬编码密码、token、密钥、API key',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
trigger: ['code_implementation', 'doc_update', 'config_change', 'commit'],
|
|
40
|
+
enforcement: 'credential-scan',
|
|
41
|
+
description: `凭证(密码/token/密钥/API key)必须从环境变量或加密配置读取,严禁以明文形式出现在任何源代码文件中。包括但不限于:源码、Markdown 文档、YAML/JSON 配置、脚本文件。模板文件(.example/.sample/.template)允许占位符但不得包含真实值。建议配合 pre-commit hook 做自动扫描。`,
|
|
42
|
+
},
|
|
43
|
+
|
|
28
44
|
/**
|
|
29
45
|
* 禁止简化测试
|
|
30
46
|
* 原因:质量底线,测试困难必须解决
|