@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
|
@@ -9,18 +9,18 @@
|
|
|
9
9
|
* 要么声明是人读面独有的派生量(derived,须在测试的豁免表逐条登记理由)。
|
|
10
10
|
* - **角色→样式单表 + 键→中文 label 映射**:维度 label、成熟度颜色、规则 label 的唯一权威。
|
|
11
11
|
* - **json / 人读的唯一分派与退出码**:`--json` 打 `data` 的序列化,否则渲染 display model。
|
|
12
|
-
* - **路径解析与 store 构造**:`-p` / `KNOWLEDGE_BASE_DIR` /
|
|
12
|
+
* - **路径解析与 store 构造**:`-p` / `KNOWLEDGE_BASE_DIR` / 缺省目录解析只在这一处
|
|
13
|
+
* (ADR-0034:旧 ~/.studio/knowledge 兼容沿用已退役,studio 正本由编排侧显式注入)。
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import chalk from 'chalk';
|
|
16
|
-
import * as fs from 'fs';
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { FileKnowledgeStore } from '../../knowledge/store';
|
|
20
20
|
import { AUDIT_RULE_LABELS } from '../../knowledge/audit-scoring';
|
|
21
21
|
import type { AuditIssue, AuditReport, AuditRuleName } from '../../knowledge/audit-scoring';
|
|
22
22
|
import type { MaturityLevel } from '../../knowledge/types';
|
|
23
|
-
import { log,
|
|
23
|
+
import { log, type CommandIO, type CommandResult } from '../command-contract';
|
|
24
24
|
|
|
25
25
|
// ── display model ─────────────────────────────────────────
|
|
26
26
|
|
|
@@ -176,20 +176,8 @@ export function emitKnowledgeView<T>(io: CommandIO, options: { json?: boolean },
|
|
|
176
176
|
|
|
177
177
|
// ── 路径解析与 store 构造 ─────────────────────────────────
|
|
178
178
|
|
|
179
|
-
/** 缺省知识库数据根(相对用户 home) */
|
|
179
|
+
/** 缺省知识库数据根(相对用户 home)。裸 CLI 项目缺省目录;studio 场景唯一正本 ~/.studio/knowledge 由编排侧经 KNOWLEDGE_BASE_DIR 显式注入(ADR-0034) */
|
|
180
180
|
const KNOWLEDGE_DATA_DIR = path.join('.harness', 'knowledge');
|
|
181
|
-
/** 旧缺省数据根(曾寄居 studio home),仍有数据时兼容沿用,免迁移 */
|
|
182
|
-
const LEGACY_KNOWLEDGE_DATA_DIR = path.join('.studio', 'knowledge');
|
|
183
|
-
|
|
184
|
-
let legacyKnowledgeDirNotified = false;
|
|
185
|
-
|
|
186
|
-
function hasKnowledgeData(dir: string): boolean {
|
|
187
|
-
try {
|
|
188
|
-
return fs.existsSync(dir) && fs.readdirSync(dir).length > 0;
|
|
189
|
-
} catch {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
181
|
|
|
194
182
|
export interface KnowledgePathOptions {
|
|
195
183
|
/** 覆盖知识库目录(audit/snapshot/migrate/index/health 收) */
|
|
@@ -200,22 +188,15 @@ export interface KnowledgePathOptions {
|
|
|
200
188
|
|
|
201
189
|
/**
|
|
202
190
|
* 知识库数据根的唯一解析点。`-p` 优先,其次 KNOWLEDGE_BASE_DIR,
|
|
203
|
-
* 最后落到用户 home
|
|
191
|
+
* 最后落到用户 home 缺省目录(ADR-0034 目录收编:旧 ~/.studio/knowledge
|
|
192
|
+
* 兼容沿用逻辑已退役——双目录分叉是历史包袱,studio 正本由编排侧显式注入;
|
|
193
|
+
* io 参数随兼容提示一并失去用途,签名保留免动全部调用方)。
|
|
204
194
|
*/
|
|
205
|
-
export function resolveKnowledgeBaseDir(options: KnowledgePathOptions,
|
|
195
|
+
export function resolveKnowledgeBaseDir(options: KnowledgePathOptions, _io: CommandIO): string {
|
|
206
196
|
if (options.dir) return options.dir;
|
|
207
197
|
if (options.projectPath) return `${options.projectPath}/.harness/knowledge`;
|
|
208
198
|
if (process.env.KNOWLEDGE_BASE_DIR) return process.env.KNOWLEDGE_BASE_DIR;
|
|
209
|
-
|
|
210
|
-
const legacyDir = path.join(os.homedir(), LEGACY_KNOWLEDGE_DATA_DIR);
|
|
211
|
-
if (hasKnowledgeData(legacyDir)) {
|
|
212
|
-
if (!legacyKnowledgeDirNotified) {
|
|
213
|
-
legacyKnowledgeDirNotified = true;
|
|
214
|
-
logError(io, toneText(`⚠ 缺省知识库根已改为 ${defaultDir};旧目录 ${legacyDir} 仍有数据,本次沿用(免迁移,可用 KNOWLEDGE_BASE_DIR 覆盖)`, 'warn'));
|
|
215
|
-
}
|
|
216
|
-
return legacyDir;
|
|
217
|
-
}
|
|
218
|
-
return defaultDir;
|
|
199
|
+
return path.join(os.homedir(), KNOWLEDGE_DATA_DIR);
|
|
219
200
|
}
|
|
220
201
|
|
|
221
202
|
/** 知识库句柄的唯一构造点(路径解析全权交给 resolveKnowledgeBaseDir) */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import * as fs from 'fs/promises';
|
|
9
9
|
import { ConstraintChecker } from '../../core/constraints/checker';
|
|
10
|
-
import {
|
|
10
|
+
import { getMergedConstraintsConfig } from '../../core/effective-constraints';
|
|
11
11
|
import { TraceCollector } from '../../monitoring/traces';
|
|
12
12
|
import type { ConstraintContext } from '../../types/constraint';
|
|
13
13
|
import { log, processIO, type CommandIO, type CommandResult } from '../command-contract';
|
|
@@ -50,7 +50,11 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
|
|
|
50
50
|
// #139:收集器锚根构造——不传 projectPath 时 trace 会落进调用方 cwd,B 侧读不到
|
|
51
51
|
const checker = new ConstraintChecker(new TraceCollector({ projectPath }));
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
// 生效集口径与 check.ts 同一入口(ADR-0033:应用层约束进 report 面的前提)——
|
|
54
|
+
// 此前跑的是内置全集(CONSTRAINTS),config.yml 禁用与应用层条目都不进报告
|
|
55
|
+
const merged = getMergedConstraintsConfig(projectPath);
|
|
56
|
+
const effectiveConstraints = Object.values(merged.constraints);
|
|
57
|
+
const totalConstraints = effectiveConstraints.length;
|
|
54
58
|
|
|
55
59
|
const context: ConstraintContext = {
|
|
56
60
|
operation: 'file_modification',
|
|
@@ -58,7 +62,7 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
|
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
// 收集模式(架构评审候选1):不抛、全量收集,error 级违规如实进报告
|
|
61
|
-
const result = await checker.collectConstraints(context);
|
|
65
|
+
const result = await checker.collectConstraints(context, merged);
|
|
62
66
|
|
|
63
67
|
const failedErrors = result.errors.filter(r => !r.satisfied);
|
|
64
68
|
const failedWarnings = result.warnings.filter(r => !r.satisfied);
|
|
@@ -74,8 +78,8 @@ export async function report(options: ReportOptions, io: CommandIO = processIO):
|
|
|
74
78
|
projectPath,
|
|
75
79
|
constraints: {
|
|
76
80
|
total: totalConstraints,
|
|
77
|
-
errors:
|
|
78
|
-
warnings:
|
|
81
|
+
errors: effectiveConstraints.filter(c => c.severity === 'error').length,
|
|
82
|
+
warnings: effectiveConstraints.filter(c => c.severity === 'warning').length,
|
|
79
83
|
passed: result.passed ? totalConstraints : totalConstraints - violations.length,
|
|
80
84
|
failed: failedErrors.length,
|
|
81
85
|
warningViolations: failedWarnings.length,
|
package/src/cli/state-io.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `.harness/.state.json` 是 harness **自身**的运行期状态(与 RunEnv 观察的项目上行
|
|
5
5
|
* 数据不是一类东西,RunEnv 保持 ADR-0023 的只读契约)。本模块是它读-改-写的唯一
|
|
6
|
-
* 入口:`check`
|
|
6
|
+
* 入口:`check` 的智能提示、`status` 与 `constraints report`/`retire` 的观察名单
|
|
7
|
+
* (块 3 子项 4)都经本接缝,命令函数以可选参数注入,
|
|
7
8
|
* 缺省 = 真实 fs 实现(照 `CommandIO` 的注入模式,但不扩 `CommandIO`——输出面与
|
|
8
9
|
* 状态面是两个概念)。
|
|
9
10
|
*
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
|
|
13
14
|
import * as fs from 'fs';
|
|
14
15
|
import * as path from 'path';
|
|
16
|
+
import type { ConstraintWatchlistState } from '../core/constraints/usage-report';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* `.harness/.state.json` 的具名类型(从 check.ts 内联类型提出,ADR-0026 决策 1)。
|
|
@@ -23,6 +25,11 @@ export interface HarnessState {
|
|
|
23
25
|
shownHints?: string[];
|
|
24
26
|
/** 最近一次 `harness status` 运行时刻(ISO 串) */
|
|
25
27
|
lastStatusRun?: string;
|
|
28
|
+
/**
|
|
29
|
+
* 零拦截观察名单(ADR-0032,块 3 子项 4):constraintId → 列入时刻。
|
|
30
|
+
* `constraints report` / `constraints retire` 经本接缝读-改-写(只增不删)。
|
|
31
|
+
*/
|
|
32
|
+
constraintWatchlist?: ConstraintWatchlistState;
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/** harness 自身运行期状态文件的可注入接缝(读-改-写的唯一入口) */
|
package/src/core/CONTEXT.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# core/
|
|
2
2
|
|
|
3
3
|
## 职责
|
|
4
|
-
约束引擎核心:severity 显式约束系统(ADR-0029,三层命名与 kind 二元已由 ADR-0001
|
|
4
|
+
约束引擎核心:severity 显式约束系统(ADR-0029,三层命名与 kind 二元已由 ADR-0001 迁至本模型)、两层约束模型(ADR-0033:内置 definitions + 应用层 `.harness/constraints.yml`,app-constraints-loader 加载校验,填空式 checker 模板住 constraints/checkers/templated/)、生效集合并(effective-constraints)、检查点验证器(CSO/passes-gate)、Spec 验证器、项目配置加载。
|
|
5
5
|
|
|
6
6
|
## 核心导出
|
|
7
|
-
- `constraints/` — 约束定义(CONSTRAINTS 单桶,severity 在条目上,ADR-0029) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 标记区间写入器(injection-writer:「声明→对照→幂等替换」中 marker-range 替换的唯一 writer——replaceStandaloneRange/replaceEnclosedRange/cutMarkerBlock + 半标记守护;治理正本探测 GOVERNANCE_HEADING/hasGovernanceContract/countGovernanceHeadings/hasPreserveBlock 同住此模块,ADR-0011/#83。ADR-0029:注入渲染(injection-renderer)、漂移校验(injection-drift)、Agent prompt 渲染(agent-prompt-renderer) 与注入段落点路由已随文本注入层关停整体删除)/trigger 谓词(triggers.ts——harness#105 定语义,约束域谓词不住 utils 层)/使用统计(usage-report:constraints report·retire 的只读数据层,#602 起 buildConstraintsUsageReport/diagnoseRetireCandidates 等经包根公共导出——studio E1 (a) 链路消费,读 trace 两个入口——`readProjectTracesReport()` 带 `skippedLines`、`readProjectTraces()` 兼容包装丢计数;报告模型 `ConstraintsUsageReport` 的降级位 = `traceFileExists` + `skippedLines`,harness#100
|
|
7
|
+
- `constraints/` — 约束定义(CONSTRAINTS 单桶,severity 在条目上,ADR-0029) + 检查引擎(ConstraintChecker,拦截统一由 checkBeforeExecution 承担,ADR-0004) + 缓存(CheckCache:TTL 缓存 + 计数采样,H6/G5 起公开导出) + 标记区间写入器(injection-writer:「声明→对照→幂等替换」中 marker-range 替换的唯一 writer——replaceStandaloneRange/replaceEnclosedRange/cutMarkerBlock + 半标记守护;治理正本探测 GOVERNANCE_HEADING/hasGovernanceContract/countGovernanceHeadings/hasPreserveBlock 同住此模块,ADR-0011/#83。ADR-0029:注入渲染(injection-renderer)、漂移校验(injection-drift)、Agent prompt 渲染(agent-prompt-renderer) 与注入段落点路由已随文本注入层关停整体删除)/trigger 谓词(triggers.ts——harness#105 定语义,约束域谓词不住 utils 层)/使用统计(usage-report:constraints report·retire 的只读数据层,#602 起 buildConstraintsUsageReport/diagnoseRetireCandidates 等经包根公共导出——studio E1 (a) 链路消费,读 trace 两个入口——`readProjectTracesReport()` 带 `skippedLines`、`readProjectTraces()` 兼容包装丢计数;报告模型 `ConstraintsUsageReport` 的降级位 = `traceFileExists` + `skippedLines`,harness#100;零拦截观察名单中间态(ADR-0032,块 3 子项 4)——命中先进名单挂 90 天(WATCHLIST_PERIOD_DAYS)、期满且样本足才转退役候选,名单状态快照经 DiagnoseWatchOptions 入参、`watchlist`/`nextWatchlist` 出参,持久化(`.harness/.state.json`)归 cli 层 StateIO,本层零 fs 写)/清单对照(capabilities-reconcile:CAPABILITIES.md 与代码实况的覆盖/幽灵判定唯一实现,capability_sync·docs_freshness·sync-docs 共消费,ADR-0009;一次 check run 内该判定只算一次并由 `RunEnv.capabilities()` 供给两 checker,ADR-0023 决策 4——增量那一维由 capability_sync 用导出的唯一覆盖规则 `isCoveredByEntries` 自算,代码实况清单的唯一取法是 `collectPopulationFiles`);CheckEnv 生产侧唯一构造点 `buildCheckEnv(context, providers|'none', runEnv?)`(checkers/types.ts:CheckEnv `extends RunEnv`,runEnv 注入即与 context-builder 共用同一份上行数据读取、不传则自造一枚;providers = 证据接线,git 两项绑 GitEvidence 实例,'none' = 显式不接 → evidence flag checker 按契约 skip);判定接缝 `CheckOutcome = boolean | 'skip' | CheckDetail{pass, evidence}`(harness#119/ADR-0016)——编排层与 `gates/checker-gate` 一律经唯一归一点 `normalizeCheckOutcome()` 取三态 + 证据,不各自 `=== false`(对新形状会误判);证据行由 checker 自行措辞(自描述、不带缩进),组装与截断策略单点在 `formatEvidence()`/`MAX_EVIDENCE_ITEMS`(同住 checkers/types.ts);证据四出口 = `ConstraintResult.evidence` → CLI 结论块/提示块、`ExecutionTrace.evidence`、error 级 `ConstraintViolationError.message`(error 级违规直接 throw,异常文案是其证据唯一外溢面);检查引擎双出口——`checkConstraints`(block:首个 error 级违规即抛,throw 契约不动)与 `collectConstraints`(collect:共享检查体、不抛、全量收集,report 的不抛出口,架构评审候选1)
|
|
8
8
|
- `constraints/context-reconcile.ts` — CONTEXT.md「核心导出」节与目录导出面的**幽灵/未登记**判定唯一实现(harness#142 / ADR-0025,与 `capabilities-reconcile` 同一套覆盖/幽灵语义,不另造第二套对照逻辑):`reconcileContext({ contextMdContent, exportSurface, barrelExports? })` → `{ declaredSymbols, ghosts, unlistedBarrelExports, hasCoreExportsSection }`;配套两个口径正本也是纯函数——`parseDeclaredExportSymbols`(只认 bullet 行首反引号标识符,`/` 连接视为多声明,形参与 `(type)`注解剥掉,非标识符的模块名 bullet 整条跳过)与 `parseExportStatements`(声明形 / `export { a, b as c }` / 逐项与块级 `type` / `export default` / `export *` 的待解析 spec)。零 fs IO:文件遍历与 `export *` 目标解析在调用方(`cli/commands/sync-docs/context-syncer.ts` 的 `collectContextExportSurface`);覆盖方向只判 barrel 的**值**符号(类型面豁免),mtime 判定就此降级为本地提示。本层 checker 暂无消费方
|
|
9
9
|
- `git-evidence.ts` — check 链路的 git 取证唯一接缝(#87,范围按 ADR-0021 收窄;`spec/validator` 的 staged 列表同型复用):`createGitEvidence(projectPath, run?)` 返回 `GitEvidence{stagedDiff, changedFileNames(staged), headDirs}`,seam 上两个真 adapter(缺省 `realGitCommandRunner` 走真 git / 注入 runner 供计数与替身)。命令级 memo 落在此实例(含失败结果),实例生命周期 = 一次 check run,故 context-builder 与 ConstraintChecker 传同一实例即共用证据;判定语义与迁移前一致(diff 取证失败 → 空串,`git ls-tree` 失败 → null = 所有目录视为新);`parseHeadDirs`/`splitFileNames` 是配套纯函数
|
|
10
10
|
- `constraints/context-builder.ts` — 只装配不取证:`buildConstraintContext(options.evidence?, options.runEnv?)` 与 `detectTrigger(…, options.runEnv?)` 经 GitEvidence 取 git 事实(不传 = 自建真 adapter),本模块无 child_process import;上行数据(源码根、trace 记录)经 `RunEnv` 取(ADR-0023),其余证据标志(ROOT_CAUSE/reuse/需求文档标记)仍为本地探测;`detectFailingTest`/`detectVerificationEvidence` 是模块私有的布尔投影(除本模块外零消费者,收私依 ADR-0022 同判据)
|
|
11
11
|
- `constraints/run-env.ts` — 一次 check 的运行级观察面(ADR-0023):`createRunEnv(projectPath)` 返回 `RunEnv{projectPath, traceTail(limit), sourceRoots(), rawConfig(), capabilities(populationFiles)}`(ADR-0029:customConstraints 观察口随 custom 约束面退役移除),懒建 + run 内 memo,同一份文件一次运行至多读一次。与 git-evidence 同一形状:一次 run 一份、沿调用链显式传递(CLI check 入口 → 生效集 / buildConstraintContext / checkConstraints),不做模块级全局。为何是两个类型而不是一个:`CheckEnv` 含 `context`,而 `context` 正是本对象读文件算出来的产物——同一对象不能既是构造者的输入又是其产物;故这里只放「不需要 context 就能造」的那半,`CheckEnv` 以 `extends RunEnv` 从它派生(`buildCheckEnv(context, evidence, runEnv?)` 第三参注入,不传 = 自造一枚一次性观察面)。配置口口径:`rawConfig()` 缺失 → undefined、空文件 → `{}`、YAML 解析失败**照抛**(兜与否属消费方判定)。`capabilities(population)` 一次读文档 + 一次 `reconcileCapabilities`(带存在性 oracle;不收 git 增量,`verdict.uncoveredChanges` 在此恒空),**memo 只记第一次传入的清单**——两消费方一律经 `collectPopulationFiles` 取数以由构造保证同形;源根 memo 与它共用。`traceTail(limit)` 底层是 `jsonl.readJsonlWindow`(一次分块读尾部行文本、按不同 limit 反复截窗),**先截行文本再 parse** 才与「各读一遍」逐字同口径(坏行占槽位不占 records 名额);请求超出窗口上限直接抛,不静默少给。`RunTarget = RunEnv | 项目根路径` 与 `resolveRunEnv()` 是这套入参形状的归一点:传路径 = 一次性读取语义(每次调用读当下内容)
|
|
12
|
-
- `effective-constraints.ts` — `getEffectiveConstraints(target, {preset?})`:全仓唯一生效集来源(内置 → preset → config.yml 禁用;ADR-0029:custom 追加与 scenes 过滤随文本注入层关停退役);`constraintsFromMerged(merged)`:从已算好的合并配置取清单(推导单点,`getEffectiveConstraints` 也走它);`getMergedConstraintsConfig(target, {preset?})`:同链路的完整 MergedConstraintsConfig 形状(含 constraints/disabled/unknownIds 单桶),内置工单 23 优先级规则(**只有显式传 preset 才覆盖 config.yml**,CLI 不给缺省值,ADR-0023 步骤 4.5),check 命令经此入口 → 一次运行只算一遍;三个入口的 `target` = `RunTarget`(传 RunEnv 即与 checker 共用同一份配置读取,ADR-0023 决策 2);`lintEffectiveConfig` 配置诊断(unknownIds)
|
|
12
|
+
- `effective-constraints.ts` — `getEffectiveConstraints(target, {preset?})`:全仓唯一生效集来源(内置 → preset → 并入应用层(ADR-0033)→ config.yml 禁用;ADR-0029:custom 追加与 scenes 过滤随文本注入层关停退役);`constraintsFromMerged(merged)`:从已算好的合并配置取清单(推导单点,`getEffectiveConstraints` 也走它);`getMergedConstraintsConfig(target, {preset?})`:同链路的完整 MergedConstraintsConfig 形状(含 constraints/disabled/unknownIds 单桶),内置工单 23 优先级规则(**只有显式传 preset 才覆盖 config.yml**,CLI 不给缺省值,ADR-0023 步骤 4.5),check 命令经此入口 → 一次运行只算一遍;三个入口的 `target` = `RunTarget`(传 RunEnv 即与 checker 共用同一份配置读取,ADR-0023 决策 2);`lintEffectiveConfig` 配置诊断(unknownIds)
|
|
13
13
|
- `effective-set.ts` — `filterEnabledEntries(knownIds, entries)`:config 条目筛选器唯一实现,语义单一——未知 id 静默忽略并收集进 `unknownIds` 供诊断(历史配置可能残留已移除条目的禁用键,故不抛错)。唯一生产调用方是约束侧 `mergeConstraints`;曾按语义分 `'collect'`/`'throw'` 两模式,throw 侧唯一调用方 `getEffectiveGates` 随 ADR-0002「后续变更」收缩后,该模式按 ADR-0022 判据删除(harness#141)
|
|
14
14
|
- `validators/` — checkpoints、passes-gate、CSO 验证器;`test-output.ts` —「测试产物解读」唯一实现:`extractFailures` 提取函数 + `judgeTestRun` 判定入口(退出码为主 + 行首锚定的文本失败信号交叉否决;文本救不回非零退出,`allowPartialPass` 只作用于文本否决那一维),passes-gate 与 `gates/acceptance` 的结论一律从它取、各自体内不写判定 `if`(解析收口 ADR-0012,判定依据 ADR-0014);**不读覆盖率**——harness 核心判定对覆盖率不取数不执法,判覆盖率的两处(CLI `passes-gate --coverage`、`gates/performance`)各自读 json-summary,`extractCoverage` 与 `TaskTestResult.coverage` 已随 ADR-0015 删除;`PassesGate.runTests(workDir)` 的执行与证据落盘根是**必传形参**,本层不再取 cwd(harness#95,约定正本见 `src/cli/commands/CONTEXT.md`);`detectTestCommand` 是测试命令探测唯一正本(模块级导出:test:ci → test 排 echo 占位 → test:e2e → test:coverage → Python 标记 pyproject.toml/pytest.ini → go.mod,探不到返回 undefined,兜底归调用方——CLI 映射 skip、PassesGate 内部 fail-closed,不再兜底 'npm test';CLI 本地版已删,架构评审 A2);PassesGate 扩展注册表(registerExtension/unregisterExtension/getExtensionNames/runAllTests/checkAllPasses + PassesGateExtension/ExtensionTestResult 两类型与包根导出)已整段删除——零 adapter 假想 seam 且扩展结果本就不参与 allowed 判定,需要扩展测试类型时按 ADR-0014 判例在 `judgeTestRun` 层加,不恢复旧 seam(harness#104,架构评审 B1);PassesGate 公开面 = `check()` + `runTests()` 两个真实消费点,`setPasses`/`getTestResult`/`checkTestFileChanges` 三方法与死数据 `PROTECTED_TEST_PATTERNS`、私有 `testResults` 缓存已随 ADR-0022(双仓零消费者)删除,历史 AC-007「保留 setPasses 向后兼容」同案推翻;`runTest` 的 `_task` 形参与其类型 `DynamicTask` 是同批漏收的第三处死面(二次复审 B1)——setPasses 删除后只剩这个从不读取的私有形参在供养它,而包根 `DynamicTask` 取自 `./types/passes-gate`,故它是**公开的**死类型;随 A1/A2 同判据删除,`runTest` 签名收为 `(workDir, command?)`,类型面钉子同 `PassesGateResult` 手法(`src/__tests__/public-exports.test.ts`);包内模块,不进 `src/index.ts` 导出面;`check-handlers/` 各族处理器统一签名 `(check, context)`,context 即既有注入车辆(`customHandlers` 同形)——`http_*` 族的重试退避等待经 `CheckpointContext.sleep` 注入(工单 #136),缺省 = 真定时器且退避公式与重试次数逐字不变,测试据此断言尝试次数与退避序列而不必真等;新增等待类副作用按同形收进 context,不再内联进处理器
|
|
15
15
|
- `spec/validator` — SpecValidator
|
|
16
|
-
- `
|
|
16
|
+
- `app-constraints-loader.ts` — 应用层约束加载器(ADR-0033 两层约束模型):读应用仓 `.harness/constraints.yml` → 加载期校验(id 强制 `app_` 前缀且不与内置冲突、checker 模板须在 `constraints/checkers/templates-registry.ts` 的 TEMPLATES 注册且 validateParams 通过、id/rule/checker/severity 必填、坏 YAML 抛错;文件不存在 = 无应用层约束)→ 实例化 `Constraint[]`(source='app',trigger 缺省全操作集);memo 口径与 `RunEnv.rawConfig()` 同形(WeakMap 挂观察面实例,传路径 = 每次读当下内容)。模板 checker 注册表拆两层:内置 checker 按约束 id(checkers/index.ts 对账闭环不动)+ 填空式模板按模板 id(`checkers/templated/`:regex-scan = 正则扫描(pattern 必填/glob/scope diff|content,形态参照 no-hardcoded-credentials)、file-exists = 文件存在性(path 或 glob/mustExist,形态参照 context-doc-sync));templates-registry 独立成模块是为防 loader↔checker 值级循环(loader 不得引 checkers/index.ts 桶)
|
|
17
|
+
- `project-config-loader` — 项目配置解读 + 约束合并(mergeConstraints 内含 preset 裁剪:presets/ 纯数据(键按 severity 命名 errors/warnings,ADR-0029)+ 未知名回落 standard,禁用到未知 id 经 effective-set 共享筛选器收集(未知 id 不抛错、进 `unknownIds` 供诊断);ADR-0033:mergeConstraints 在 config.yml 筛选前并入应用层条目(app-constraints-loader),enabled:false/retired 墓碑对应用层 id 同口径生效且应用层 id 不进 unknownIds);ADR-0029:custom 约束装载(loadCustomConstraints/getCustomConstraints/toConstraint)与退役判定单点 `isConstraintRetired` 已随 custom 面退役移除,retire 的 already_retired 判定只剩 config.yml `enabled:false` 一处;**本模块不碰文件系统**:config.yml 的读取在 `constraints/run-env.ts`(ADR-0023 决策 2 撤销了工单 16 的进程级 `rawConfigCache` + mtime 指纹,改为一次运行一份快照);访问器族(`loadRawProjectConfig`/`getGovernanceConfig`/`resolveContextFiles`/`getCapabilitiesMode`)与 `ProjectConfigLoader` 构造器的入参形状 = `RunTarget`(传项目根路径 = 一次性读取,传 RunEnv = 与本 run 其余消费方共用同一份);`getGovernanceConfig` 是全仓唯一允许手写钻取 `config.governance` 的点(工单 84),消费方一律经它或经 `resolveContextFiles`(三态:unconfigured / enabled-empty / enabled,dirs 元素类型在此收口)取数据,不得再自己 cast `Record<string, unknown>`;checker 对前两态一律 skip,自动探测回落只属于 init/扫描类工具流调用方
|
|
17
18
|
- `index.ts` barrel 公开面(子路径 `@dommaker/harness/core`,ADR-0003 显式清单、禁 `export *`)——定义取数 `getAllConstraints()` / `getConstraint(id)` / `findConstraintsByTrigger(trigger)`(正本 `constraints/definitions.ts`);判定出口 `checkConstraints()` / `checkConstraint()` / `checkBeforeExecution()` 与未接线默认实例 `constraintChecker`(= `ConstraintChecker.getInstance()`,不写 trace,正本 `constraints/checker.ts`);约束数据 `CONSTRAINTS` 单桶(正本 `constraints/definitions.ts`;ADR-0029:注入渲染三件套导出已随文本注入层关停移除);验证器 `CheckpointValidator` / `CSOValidator`(两者私有构造 + `static getInstance()` 单例出口)与 `PassesGate` + 工厂 `createPassesGate(config?)`(`Partial<PassesGateConfig>`,正本 `validators/`)
|
|
18
19
|
|
|
19
20
|
## 依赖关系
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用层约束加载器(ADR-0033 两层约束模型)
|
|
3
|
+
*
|
|
4
|
+
* 读应用仓 `.harness/constraints.yml` → 校验 → 实例化 `Constraint[]`(source='app')。
|
|
5
|
+
* 职责切分:config.yml = 对内置/应用层约束的裁剪与墓碑;constraints.yml = 应用层约束的定义正本。
|
|
6
|
+
*
|
|
7
|
+
* 接口规则(调用方须知):
|
|
8
|
+
* - 文件不存在 = 无应用层约束,正常返回空;文件存在但 YAML 解析失败/缺必填字段 → 抛错
|
|
9
|
+
* (配置坏不能静默放行,延续注册表闭环语义)
|
|
10
|
+
* - id 强制 `app_` 前缀;与内置 id 冲突或无前缀 → 加载期抛错
|
|
11
|
+
* - checker 必须是模板注册表(checkers/index.ts TEMPLATES)里的 id,且 validateParams 通过,
|
|
12
|
+
* 否则加载期抛错
|
|
13
|
+
* - 必填字段:id / rule / checker / severity;可选:params / trigger / message / description /
|
|
14
|
+
* enforcement。trigger 缺省 = 每次 check 都评估(全操作集)
|
|
15
|
+
*
|
|
16
|
+
* memo 口径与 `RunEnv.rawConfig()` 同形:传 RunEnv 即一枚观察面至多读一次
|
|
17
|
+
* (WeakMap 挂在实例上);传项目根路径 = 自造一枚一次性观察面,每次调用读当下内容。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import * as yaml from 'js-yaml';
|
|
23
|
+
import type { Constraint, ConstraintSeverity, ConstraintTrigger } from '../types/constraint';
|
|
24
|
+
import { CONSTRAINTS } from './constraints/definitions';
|
|
25
|
+
// 模板注册表直引 templates-registry 而非 checkers 桶:checkers/index.ts 经内置
|
|
26
|
+
// checker 回头依赖 project-config-loader,从本模块引桶会成值级循环
|
|
27
|
+
import { TEMPLATES } from './constraints/checkers/templates-registry';
|
|
28
|
+
import { resolveRunEnv, type RunEnv, type RunTarget } from './constraints/run-env';
|
|
29
|
+
|
|
30
|
+
/** 应用层约束定义文件在项目根下的相对路径 */
|
|
31
|
+
const APP_CONSTRAINTS_FILE_REL = path.join('.harness', 'constraints.yml');
|
|
32
|
+
|
|
33
|
+
/** id 强制前缀 */
|
|
34
|
+
const APP_ID_PREFIX = 'app_';
|
|
35
|
+
|
|
36
|
+
const SEVERITIES: readonly ConstraintSeverity[] = ['error', 'warning', 'info'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* trigger 缺省值 = 全操作集:应用层约束未声明 trigger 时每次 check 都评估
|
|
40
|
+
* (填空式模板多为扫描/存在性形态,与 no_hardcoded_credentials 同类,不猜触发域)
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_TRIGGERS: ConstraintTrigger[] = [
|
|
43
|
+
'code_implementation',
|
|
44
|
+
'test_creation',
|
|
45
|
+
'module_creation',
|
|
46
|
+
'module_modification',
|
|
47
|
+
'module_extension',
|
|
48
|
+
'module_deletion',
|
|
49
|
+
'file_modification',
|
|
50
|
+
'doc_update',
|
|
51
|
+
'config_change',
|
|
52
|
+
'commit',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** run 内 memo(与 RunEnv.rawConfig 同形:一枚观察面至多读一次) */
|
|
56
|
+
const memo = new WeakMap<RunEnv, Constraint[]>();
|
|
57
|
+
|
|
58
|
+
class AppConstraintsError extends Error {}
|
|
59
|
+
|
|
60
|
+
function fail(rel: string, detail: string): never {
|
|
61
|
+
throw new AppConstraintsError(`[harness] 应用层约束加载失败(${rel}):${detail}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function asString(value: unknown): string | undefined {
|
|
65
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 校验并实例化单条应用层约束条目 */
|
|
69
|
+
function toConstraint(raw: unknown, index: number, rel: string): Constraint {
|
|
70
|
+
const at = `constraints[${index}]`;
|
|
71
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
72
|
+
fail(rel, `${at} 必须是对象`);
|
|
73
|
+
}
|
|
74
|
+
const entry = raw as Record<string, unknown>;
|
|
75
|
+
|
|
76
|
+
const id = asString(entry.id);
|
|
77
|
+
if (!id) fail(rel, `${at} 缺必填字段 id`);
|
|
78
|
+
// 冲突判定在前:撞上内置 id 的条目报"冲突"比报"缺前缀"更指向根因
|
|
79
|
+
if (CONSTRAINTS[id]) {
|
|
80
|
+
fail(rel, `${at} id "${id}" 与内置约束冲突`);
|
|
81
|
+
}
|
|
82
|
+
if (!id.startsWith(APP_ID_PREFIX)) {
|
|
83
|
+
fail(rel, `${at} id "${id}" 必须有 ${APP_ID_PREFIX} 前缀(应用层约束命名空间)`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const rule = asString(entry.rule);
|
|
87
|
+
if (!rule) fail(rel, `${at} (${id}) 缺必填字段 rule`);
|
|
88
|
+
|
|
89
|
+
const checker = asString(entry.checker);
|
|
90
|
+
if (!checker) fail(rel, `${at} (${id}) 缺必填字段 checker`);
|
|
91
|
+
const factory = TEMPLATES.get(checker);
|
|
92
|
+
if (!factory) {
|
|
93
|
+
fail(rel, `${at} (${id}) 引用的 checker 模板 "${checker}" 未注册(可用模板见 checkers/templated/)`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const severity = entry.severity;
|
|
97
|
+
if (!SEVERITIES.includes(severity as ConstraintSeverity)) {
|
|
98
|
+
fail(rel, `${at} (${id}) 缺必填字段 severity 或取值非法(须为 ${SEVERITIES.join('/')})`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const params = entry.params;
|
|
102
|
+
if (params !== undefined && (params === null || typeof params !== 'object' || Array.isArray(params))) {
|
|
103
|
+
fail(rel, `${at} (${id}) params 必须是对象`);
|
|
104
|
+
}
|
|
105
|
+
const paramErrors = factory.validateParams((params ?? {}) as Record<string, unknown>);
|
|
106
|
+
if (paramErrors.length > 0) {
|
|
107
|
+
fail(rel, `${at} (${id}) 模板 "${checker}" 参数校验失败:${paramErrors.join(';')}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const trigger = entry.trigger;
|
|
111
|
+
if (trigger !== undefined && typeof trigger !== 'string' &&
|
|
112
|
+
!(Array.isArray(trigger) && trigger.every(t => typeof t === 'string'))) {
|
|
113
|
+
fail(rel, `${at} (${id}) trigger 必须是字符串或字符串数组`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
kind: 'check',
|
|
119
|
+
rule,
|
|
120
|
+
message: asString(entry.message) ?? rule,
|
|
121
|
+
severity: severity as ConstraintSeverity,
|
|
122
|
+
trigger: (trigger as ConstraintTrigger | ConstraintTrigger[] | undefined) ?? DEFAULT_TRIGGERS,
|
|
123
|
+
enforcement: asString(entry.enforcement) ?? '',
|
|
124
|
+
description: asString(entry.description),
|
|
125
|
+
source: 'app',
|
|
126
|
+
checker,
|
|
127
|
+
params: params as Record<string, unknown> | undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 读取并实例化应用层约束(`.harness/constraints.yml`)
|
|
133
|
+
*
|
|
134
|
+
* @param target 项目根路径,或本 run 已构造的运行级观察面(与 rawConfig 同一入参形状);
|
|
135
|
+
* 传观察面 = 与本 run 其余消费方共用「一次运行至多读一次」语义
|
|
136
|
+
*/
|
|
137
|
+
export function loadAppConstraints(target: RunTarget): Constraint[] {
|
|
138
|
+
const env = resolveRunEnv(target);
|
|
139
|
+
const cached = memo.get(env);
|
|
140
|
+
if (cached) return cached;
|
|
141
|
+
|
|
142
|
+
const rel = APP_CONSTRAINTS_FILE_REL;
|
|
143
|
+
const file = path.join(env.projectPath, rel);
|
|
144
|
+
let constraints: Constraint[] = [];
|
|
145
|
+
if (fs.existsSync(file)) {
|
|
146
|
+
let loaded: unknown;
|
|
147
|
+
try {
|
|
148
|
+
loaded = yaml.load(fs.readFileSync(file, 'utf-8'));
|
|
149
|
+
} catch (err) {
|
|
150
|
+
fail(rel, `YAML 解析失败:${err instanceof Error ? err.message : String(err)}`);
|
|
151
|
+
}
|
|
152
|
+
const root = (loaded ?? {}) as unknown;
|
|
153
|
+
if (root === null || typeof root !== 'object' || Array.isArray(root)) {
|
|
154
|
+
fail(rel, '顶层必须是对象(含 constraints 列表)');
|
|
155
|
+
}
|
|
156
|
+
const list = (root as Record<string, unknown>).constraints;
|
|
157
|
+
if (list !== undefined && !Array.isArray(list)) {
|
|
158
|
+
fail(rel, 'constraints 必须是数组');
|
|
159
|
+
}
|
|
160
|
+
constraints = (list ?? []).map((entry, i) => toConstraint(entry, i, rel));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
memo.set(env, constraints);
|
|
164
|
+
return constraints;
|
|
165
|
+
}
|
|
@@ -58,7 +58,9 @@ describe('collectConstraints(收集模式,不抛)', () => {
|
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
it('guidelines 照常执行,warningCount 与不满意条目同口径', async () => {
|
|
61
|
-
|
|
61
|
+
// 触发域用 module_modification:32c07ed 把 no_hardcoded_credentials 升 error 级后,
|
|
62
|
+
// code_implementation 域内已无 warning 级约束,原断言恒假
|
|
63
|
+
const result = await checker.collectConstraints({ ...VIOLATING, operation: 'module_modification' });
|
|
62
64
|
expect(result.warnings.length).toBeGreaterThan(0);
|
|
63
65
|
expect(result.warningCount).toBe(result.warnings.filter((g: ConstraintResult) => !g.satisfied).length);
|
|
64
66
|
});
|
|
@@ -110,3 +110,66 @@ describe('坏行计数透传到 report 数据层(harness#100)', () => {
|
|
|
110
110
|
expect(report.traceFileExists).toBe(false);
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
describe('应用层约束进候选诊断(ADR-0033 块 3 子项 3)', () => {
|
|
115
|
+
const APP_YML = [
|
|
116
|
+
'constraints:',
|
|
117
|
+
' - id: app_no_internal_url',
|
|
118
|
+
' rule: Web code must not contain internal URLs',
|
|
119
|
+
' checker: regex-scan',
|
|
120
|
+
' params:',
|
|
121
|
+
" pattern: 'https?://10\\.'",
|
|
122
|
+
' severity: warning',
|
|
123
|
+
'',
|
|
124
|
+
].join('\n');
|
|
125
|
+
|
|
126
|
+
it('生效集带上应用层后:stats 含应用层条目,零触发自然进候选', () => {
|
|
127
|
+
const root = createProjectFixture({
|
|
128
|
+
name: 'usage-report-app',
|
|
129
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const report = buildConstraintsUsageReport(root);
|
|
133
|
+
|
|
134
|
+
const stat = report.stats.find(s => s.id === 'app_no_internal_url');
|
|
135
|
+
expect(stat).toBeDefined();
|
|
136
|
+
expect(stat!.severity).toBe('warning');
|
|
137
|
+
expect(stat!.total).toBe(0);
|
|
138
|
+
expect(
|
|
139
|
+
report.candidates.some(c => c.id === 'app_no_internal_url' && c.kind === 'zero_trigger')
|
|
140
|
+
).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('应用层约束的 trace 统计与内置同口径聚合', () => {
|
|
144
|
+
const root = createProjectFixture({
|
|
145
|
+
name: 'usage-report-app',
|
|
146
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
147
|
+
traces: [
|
|
148
|
+
{ constraintId: 'app_no_internal_url', result: 'pass' },
|
|
149
|
+
{ constraintId: 'app_no_internal_url', result: 'fail', timestamp: 1700000001000 },
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const report = buildConstraintsUsageReport(root);
|
|
154
|
+
|
|
155
|
+
const stat = report.stats.find(s => s.id === 'app_no_internal_url')!;
|
|
156
|
+
expect(stat.total).toBe(2);
|
|
157
|
+
expect(stat.fail).toBe(1);
|
|
158
|
+
expect(stat.failRate).toBeCloseTo(0.5);
|
|
159
|
+
// 1/2 fail,低于一切候选阈值 → 不进候选
|
|
160
|
+
expect(report.candidates.some(c => c.id === 'app_no_internal_url')).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('config.yml 禁用应用层约束 → 不进 stats 与候选(生效集口径一致)', () => {
|
|
164
|
+
const root = createProjectFixture({
|
|
165
|
+
name: 'usage-report-app',
|
|
166
|
+
config: 'constraints:\n app_no_internal_url:\n enabled: false\n',
|
|
167
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const report = buildConstraintsUsageReport(root);
|
|
171
|
+
|
|
172
|
+
expect(report.stats.some(s => s.id === 'app_no_internal_url')).toBe(false);
|
|
173
|
+
expect(report.candidates.some(c => c.id === 'app_no_internal_url')).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -156,7 +156,7 @@ export class ConstraintChecker {
|
|
|
156
156
|
evidence?: GitEvidence,
|
|
157
157
|
runEnv?: RunEnv
|
|
158
158
|
): Promise<CheckOutcome> {
|
|
159
|
-
const impl = getConstraintCheck(constraint
|
|
159
|
+
const impl = getConstraintCheck(constraint);
|
|
160
160
|
if (!impl) {
|
|
161
161
|
throw new Error(
|
|
162
162
|
`[harness] 约束 "${constraint.id}" (kind='check') 未注册 checker,拒绝静默通过。` +
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 填空式 checker 模板(regex-scan / file-exists)测试(ADR-0033 块 3 子项 2)
|
|
3
|
+
*
|
|
4
|
+
* 范式同 capability-sync.test.ts:测面是 ConstraintCheck.evaluate(env),
|
|
5
|
+
* 证据经 buildCheckEnv 的 EvidenceProviders stub 注入,项目根用 createProjectFixture。
|
|
6
|
+
* 另覆盖 validateParams 各分支(加载期校验由 app-constraints-loader 套件钉住,
|
|
7
|
+
* 此处钉模板自身的参数口径)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from '@jest/globals';
|
|
11
|
+
import { regexScan } from '../templated/regex-scan';
|
|
12
|
+
import { fileExists } from '../templated/file-exists';
|
|
13
|
+
import { TEMPLATES } from '../index';
|
|
14
|
+
import { buildCheckEnv, normalizeCheckOutcome, type CheckEnv, type CheckOutcome } from '../types';
|
|
15
|
+
import { createProjectFixture } from '../../../../test-setup/project-fixture';
|
|
16
|
+
import type { ConstraintContext } from '../../../../types/constraint';
|
|
17
|
+
|
|
18
|
+
/** 证据 stub:staged diff / 变更清单由用例注入 */
|
|
19
|
+
function makeEnv(
|
|
20
|
+
projectPath: string,
|
|
21
|
+
opts: { diff?: string; changedFiles?: string[] } = {}
|
|
22
|
+
): CheckEnv {
|
|
23
|
+
const context: ConstraintContext = {
|
|
24
|
+
operation: 'commit',
|
|
25
|
+
projectPath,
|
|
26
|
+
changedFiles: opts.changedFiles ?? [],
|
|
27
|
+
};
|
|
28
|
+
return buildCheckEnv(context, {
|
|
29
|
+
stagedDiff: async () => opts.diff ?? '',
|
|
30
|
+
stagedDiffNames: async () => (opts.changedFiles ?? []).join('\n'),
|
|
31
|
+
srcScan: () => [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const passed = (outcome: CheckOutcome): boolean => normalizeCheckOutcome(outcome).satisfied;
|
|
36
|
+
const evidenceText = (outcome: CheckOutcome): string =>
|
|
37
|
+
normalizeCheckOutcome(outcome).evidence.join('\n');
|
|
38
|
+
|
|
39
|
+
const DIFF_WITH_HIT = [
|
|
40
|
+
'diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts',
|
|
41
|
+
'--- a/apps/web/src/api.ts',
|
|
42
|
+
'+++ b/apps/web/src/api.ts',
|
|
43
|
+
'@@ -1,2 +1,3 @@',
|
|
44
|
+
' const x = 1;',
|
|
45
|
+
'+const base = "http://10.0.1.8:8080";',
|
|
46
|
+
'-const old = 2;',
|
|
47
|
+
].join('\n');
|
|
48
|
+
|
|
49
|
+
describe('regex-scan 模板注册与参数校验', () => {
|
|
50
|
+
it('已注册进 TEMPLATES', () => {
|
|
51
|
+
expect(TEMPLATES.get('regex-scan')).toBe(regexScan);
|
|
52
|
+
expect(TEMPLATES.get('file-exists')).toBe(fileExists);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('validateParams:pattern 必填且须为合法正则', () => {
|
|
56
|
+
expect(regexScan.validateParams({})).not.toEqual([]);
|
|
57
|
+
expect(regexScan.validateParams({ pattern: 1 }).join()).toContain('pattern');
|
|
58
|
+
expect(regexScan.validateParams({ pattern: '([' }).join()).toContain('合法正则');
|
|
59
|
+
expect(regexScan.validateParams({ pattern: 'ok' })).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('validateParams:glob 须为字符串,scope 只收 diff/content', () => {
|
|
63
|
+
expect(regexScan.validateParams({ pattern: 'x', glob: 1 }).join()).toContain('glob');
|
|
64
|
+
expect(regexScan.validateParams({ pattern: 'x', scope: 'all' }).join()).toContain('scope');
|
|
65
|
+
expect(regexScan.validateParams({ pattern: 'x', glob: 'src/**', scope: 'content' })).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('regex-scan evaluate(scope=diff 缺省)', () => {
|
|
70
|
+
const check = regexScan.create('app_no_internal_url', { pattern: 'https?://10\\.' });
|
|
71
|
+
|
|
72
|
+
it('staged diff 新增行命中 → 违规,证据点名文件与命中行', async () => {
|
|
73
|
+
const dir = createProjectFixture({ name: 'regex-scan-diff' });
|
|
74
|
+
const outcome = await check.evaluate(makeEnv(dir, { diff: DIFF_WITH_HIT }));
|
|
75
|
+
expect(passed(outcome)).toBe(false);
|
|
76
|
+
expect(evidenceText(outcome)).toContain('apps/web/src/api.ts');
|
|
77
|
+
expect(evidenceText(outcome)).toContain('http://10.0.1.8:8080');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('删除行(- 前缀)不算命中', async () => {
|
|
81
|
+
const dir = createProjectFixture({ name: 'regex-scan-diff' });
|
|
82
|
+
const diff = DIFF_WITH_HIT.replace('+const base', '-const base');
|
|
83
|
+
expect(passed(await check.evaluate(makeEnv(dir, { diff })))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('未命中 → 通过', async () => {
|
|
87
|
+
const dir = createProjectFixture({ name: 'regex-scan-diff' });
|
|
88
|
+
const diff = 'diff --git a/a.ts b/a.ts\n+++ b/a.ts\n+const ok = 1;';
|
|
89
|
+
expect(passed(await check.evaluate(makeEnv(dir, { diff })))).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('glob 限定文件集:命中文件在 glob 外 → 通过', async () => {
|
|
93
|
+
const scoped = regexScan.create('app_scoped', { pattern: 'https?://10\\.', glob: 'apps/app/**' });
|
|
94
|
+
const dir = createProjectFixture({ name: 'regex-scan-diff' });
|
|
95
|
+
expect(passed(await scoped.evaluate(makeEnv(dir, { diff: DIFF_WITH_HIT })))).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('diff 为空 → 退化变更文件内容扫描', async () => {
|
|
99
|
+
const dir = createProjectFixture({
|
|
100
|
+
name: 'regex-scan-content',
|
|
101
|
+
files: { 'src/conf.ts': 'export const u = "http://10.9.9.9";' },
|
|
102
|
+
});
|
|
103
|
+
const outcome = await check.evaluate(makeEnv(dir, { changedFiles: ['src/conf.ts'] }));
|
|
104
|
+
expect(passed(outcome)).toBe(false);
|
|
105
|
+
expect(evidenceText(outcome)).toContain('src/conf.ts');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('scope=content:不看 diff,直接扫变更文件内容', async () => {
|
|
109
|
+
const contentCheck = regexScan.create('app_content', { pattern: 'https?://10\\.', scope: 'content' });
|
|
110
|
+
const dir = createProjectFixture({ name: 'regex-scan-content' });
|
|
111
|
+
// diff 里有命中但 scope=content 且 changedFiles 为空 → 通过
|
|
112
|
+
expect(passed(await contentCheck.evaluate(makeEnv(dir, { diff: DIFF_WITH_HIT })))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('file-exists 参数校验', () => {
|
|
117
|
+
it('path 或 glob 至少填一个;mustExist 须为布尔', () => {
|
|
118
|
+
expect(fileExists.validateParams({}).join()).toContain('至少填一个');
|
|
119
|
+
expect(fileExists.validateParams({ path: 1 }).join()).toContain('path');
|
|
120
|
+
expect(fileExists.validateParams({ path: 'a', mustExist: 'yes' }).join()).toContain('mustExist');
|
|
121
|
+
expect(fileExists.validateParams({ path: 'a' })).toEqual([]);
|
|
122
|
+
expect(fileExists.validateParams({ glob: 'docs/**' })).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('file-exists evaluate', () => {
|
|
127
|
+
it('path 存在 → 通过;不存在 → 违规且证据点名', async () => {
|
|
128
|
+
const dir = createProjectFixture({ name: 'file-exists', files: { 'AGENTS.md': '# a' } });
|
|
129
|
+
const check = fileExists.create('app_agents_md', { path: 'AGENTS.md' });
|
|
130
|
+
expect(passed(await check.evaluate(makeEnv(dir)))).toBe(true);
|
|
131
|
+
|
|
132
|
+
const missing = fileExists.create('app_missing', { path: 'AGENTS.md' });
|
|
133
|
+
const empty = createProjectFixture({ name: 'file-exists-empty' });
|
|
134
|
+
const outcome = await missing.evaluate(makeEnv(empty));
|
|
135
|
+
expect(passed(outcome)).toBe(false);
|
|
136
|
+
expect(evidenceText(outcome)).toContain('AGENTS.md');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('glob 有匹配 → 通过;无匹配 → 违规', async () => {
|
|
140
|
+
const dir = createProjectFixture({ name: 'file-exists', files: { 'docs/a/x.md': 'x' } });
|
|
141
|
+
const check = fileExists.create('app_docs', { glob: 'docs/**/*.md' });
|
|
142
|
+
expect(passed(await check.evaluate(makeEnv(dir)))).toBe(true);
|
|
143
|
+
|
|
144
|
+
const empty = createProjectFixture({ name: 'file-exists-empty' });
|
|
145
|
+
const outcome = await check.evaluate(makeEnv(empty));
|
|
146
|
+
expect(passed(outcome)).toBe(false);
|
|
147
|
+
expect(evidenceText(outcome)).toContain('docs/**/*.md');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('path 与 glob 都填 = 两者都要满足', async () => {
|
|
151
|
+
const dir = createProjectFixture({ name: 'file-exists', files: { 'AGENTS.md': '# a' } });
|
|
152
|
+
const check = fileExists.create('app_both', { path: 'AGENTS.md', glob: 'docs/**' });
|
|
153
|
+
const outcome = await check.evaluate(makeEnv(dir));
|
|
154
|
+
expect(passed(outcome)).toBe(false);
|
|
155
|
+
expect(evidenceText(outcome)).toContain('docs/**');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('mustExist=false:存在 → 违规且证据点名;不存在 → 通过', async () => {
|
|
159
|
+
const withFile = createProjectFixture({ name: 'file-exists', files: { '.env.local': 'x' } });
|
|
160
|
+
const check = fileExists.create('app_no_env_local', { path: '.env.local', mustExist: false });
|
|
161
|
+
const outcome = await check.evaluate(makeEnv(withFile));
|
|
162
|
+
expect(passed(outcome)).toBe(false);
|
|
163
|
+
expect(evidenceText(outcome)).toContain('.env.local');
|
|
164
|
+
|
|
165
|
+
const clean = createProjectFixture({ name: 'file-exists-empty' });
|
|
166
|
+
expect(passed(await check.evaluate(makeEnv(clean)))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('mustExist=false + glob:命中文件逐条进证据', async () => {
|
|
170
|
+
const dir = createProjectFixture({
|
|
171
|
+
name: 'file-exists',
|
|
172
|
+
files: { 'dist/a.js': 'x', 'dist/b.js': 'y' },
|
|
173
|
+
});
|
|
174
|
+
const check = fileExists.create('app_no_dist', { glob: 'dist/**', mustExist: false });
|
|
175
|
+
const outcome = await check.evaluate(makeEnv(dir));
|
|
176
|
+
expect(passed(outcome)).toBe(false);
|
|
177
|
+
expect(evidenceText(outcome)).toContain('dist/a.js');
|
|
178
|
+
expect(evidenceText(outcome)).toContain('dist/b.js');
|
|
179
|
+
});
|
|
180
|
+
});
|