@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
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `harness constraints report` 与 `harness constraints retire`(交互模式)共用的
|
|
5
5
|
* 数据层:读取项目 traces.log,与生效集(getEffectiveConstraints)对齐,
|
|
6
|
-
* 产出 check
|
|
6
|
+
* 产出 check 约束统计表、四类退役候选诊断与零拦截观察名单(ADR-0032/0033 块 3 子项 4:
|
|
7
|
+
* 零拦截先进观察名单挂一个季度,期满且样本足才转正式退役候选)。
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
+
* 只读:不创建目录、不写任何文件。观察名单状态以快照入参进来、以 `nextWatchlist`
|
|
10
|
+
* 产物出去,持久化(`.harness/.state.json` 经 StateIO 接缝)是 cli 层调用方的职责。
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import * as fs from 'fs';
|
|
@@ -92,6 +94,52 @@ export const DEFAULT_DIAGNOSE_THRESHOLDS: DiagnoseThresholds = {
|
|
|
92
94
|
highNoiseMinEvaluated: 20,
|
|
93
95
|
};
|
|
94
96
|
|
|
97
|
+
/**
|
|
98
|
+
* 观察名单状态条目(ADR-0032 口径,块 3 子项 4)
|
|
99
|
+
*
|
|
100
|
+
* 持久化在 `.harness/.state.json` 的 `constraintWatchlist` 段(StateIO 接缝,
|
|
101
|
+
* cli 层读-改-写);本层只认快照,不碰 fs。
|
|
102
|
+
*/
|
|
103
|
+
export interface WatchlistStateEntry {
|
|
104
|
+
/** 列入观察名单时刻(ISO 串,与 HarnessState 其余时间字段同形) */
|
|
105
|
+
listedAt: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 观察名单状态(constraintId → 条目) */
|
|
109
|
+
export type ConstraintWatchlistState = Record<string, WatchlistStateEntry>;
|
|
110
|
+
|
|
111
|
+
/** 观察期时长(天):满一个季度且样本仍达标,零拦截才转正式退役候选 */
|
|
112
|
+
export const WATCHLIST_PERIOD_DAYS = 90;
|
|
113
|
+
|
|
114
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* report 里单独分组展示的观察名单条目
|
|
118
|
+
*
|
|
119
|
+
* 列入即进 report(告知面),但不进退役候选;观察期内再次出现 fail / 样本
|
|
120
|
+
* 跌破阈值时条目自然消失(状态段里的列入记录保留,时钟不回拨)。
|
|
121
|
+
*/
|
|
122
|
+
export interface WatchlistEntry {
|
|
123
|
+
id: string;
|
|
124
|
+
stats: ConstraintUsageStats;
|
|
125
|
+
/** 列入时刻(ISO 串) */
|
|
126
|
+
listedAt: string;
|
|
127
|
+
/** 剩余观察天数(观察期未满恒 > 0) */
|
|
128
|
+
remainingDays: number;
|
|
129
|
+
/** 人类可读的诊断说明(带证据数字) */
|
|
130
|
+
reason: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 候选诊断的观察名单上下文(不传 = 空名单 + 真实时钟)
|
|
135
|
+
*/
|
|
136
|
+
export interface DiagnoseWatchOptions {
|
|
137
|
+
/** 观察名单状态快照(来自 `.harness/.state.json`,缺省空) */
|
|
138
|
+
watchlist?: ConstraintWatchlistState;
|
|
139
|
+
/** 当前时刻(Unix ms),测试注入用——别在测试里等 90 天 */
|
|
140
|
+
now?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
95
143
|
/**
|
|
96
144
|
* report 数据模型
|
|
97
145
|
*/
|
|
@@ -100,6 +148,13 @@ export interface ConstraintsUsageReport {
|
|
|
100
148
|
stats: ConstraintUsageStats[];
|
|
101
149
|
/** 退役候选诊断(按 zero_trigger → unevaluable → high_noise → zero_intercept 排序) */
|
|
102
150
|
candidates: RetireCandidate[];
|
|
151
|
+
/** 零拦截观察名单(进 report 但不进退役候选;期满且样本足才转候选) */
|
|
152
|
+
watchlist: WatchlistEntry[];
|
|
153
|
+
/**
|
|
154
|
+
* 观察名单读-改-写产物:入参快照 + 本次新列入(只增不删)。
|
|
155
|
+
* 与入参快照键数不同(有新增)时,调用方应经 StateIO 写回 `.harness/.state.json`。
|
|
156
|
+
*/
|
|
157
|
+
nextWatchlist: ConstraintWatchlistState;
|
|
103
158
|
/** 配置健康诊断(unknownIds 等) */
|
|
104
159
|
lint: EffectiveConfigLint;
|
|
105
160
|
/** trace 文件是否存在 */
|
|
@@ -176,18 +231,33 @@ function toStats(id: string, severity: Constraint['severity'], agg: { total: num
|
|
|
176
231
|
};
|
|
177
232
|
}
|
|
178
233
|
|
|
234
|
+
/** 诊断完整产物(candidates + watchlist + 待写回的状态) */
|
|
235
|
+
interface DiagnoseOutcome {
|
|
236
|
+
candidates: RetireCandidate[];
|
|
237
|
+
watchlist: WatchlistEntry[];
|
|
238
|
+
nextWatchlist: ConstraintWatchlistState;
|
|
239
|
+
}
|
|
240
|
+
|
|
179
241
|
/**
|
|
180
242
|
* 退役候选诊断(纯函数)
|
|
181
243
|
*
|
|
182
244
|
* 每条约束最多归入一个类别,优先级:
|
|
183
245
|
* zero_trigger → unevaluable → high_noise → zero_intercept
|
|
246
|
+
*
|
|
247
|
+
* zero_intercept 走观察名单中间态(ADR-0032):命中且样本够 → 不在名单则列入
|
|
248
|
+
* (listedAt = now),进 watchlist 不进 candidates;已在名单且满 WATCHLIST_PERIOD_DAYS
|
|
249
|
+
* 且当前样本仍达标 → 转正式 zero_intercept 退役候选。其他三类不进观察名单。
|
|
184
250
|
*/
|
|
185
|
-
|
|
251
|
+
function diagnose(
|
|
186
252
|
stats: ConstraintUsageStats[],
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
253
|
+
t: DiagnoseThresholds,
|
|
254
|
+
watch: DiagnoseWatchOptions
|
|
255
|
+
): DiagnoseOutcome {
|
|
256
|
+
const now = watch.now ?? Date.now();
|
|
257
|
+
const watchlistState = watch.watchlist ?? {};
|
|
258
|
+
const nextWatchlist: ConstraintWatchlistState = { ...watchlistState };
|
|
190
259
|
const candidates: RetireCandidate[] = [];
|
|
260
|
+
const watchlistEntries: WatchlistEntry[] = [];
|
|
191
261
|
|
|
192
262
|
for (const s of stats) {
|
|
193
263
|
if (s.total === 0) {
|
|
@@ -224,24 +294,66 @@ export function diagnoseRetireCandidates(
|
|
|
224
294
|
}
|
|
225
295
|
|
|
226
296
|
if (s.fail === 0 && s.evaluated >= t.zeroInterceptMinEvaluated) {
|
|
227
|
-
|
|
297
|
+
const listed = watchlistState[s.id];
|
|
298
|
+
const listedAtMs = listed ? Date.parse(listed.listedAt) : NaN;
|
|
299
|
+
// 列入时间损坏按未列入处理:重新列入、时钟重启(状态文件是人可手改的,不静默信脏值)
|
|
300
|
+
const isListed = listed !== undefined && Number.isFinite(listedAtMs);
|
|
301
|
+
|
|
302
|
+
if (isListed && now - listedAtMs >= WATCHLIST_PERIOD_DAYS * DAY_MS) {
|
|
303
|
+
candidates.push({
|
|
304
|
+
id: s.id,
|
|
305
|
+
kind: 'zero_intercept',
|
|
306
|
+
stats: s,
|
|
307
|
+
reason: `零拦截:观察期满(${listed.listedAt.slice(0, 10)} 列入,已满 ${WATCHLIST_PERIOD_DAYS} 天),${s.evaluated} 次评估从未 fail`,
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const listedAt = isListed ? listed.listedAt : new Date(now).toISOString();
|
|
313
|
+
if (!isListed) {
|
|
314
|
+
nextWatchlist[s.id] = { listedAt };
|
|
315
|
+
}
|
|
316
|
+
watchlistEntries.push({
|
|
228
317
|
id: s.id,
|
|
229
|
-
kind: 'zero_intercept',
|
|
230
318
|
stats: s,
|
|
319
|
+
listedAt,
|
|
320
|
+
remainingDays: isListed
|
|
321
|
+
? Math.max(0, Math.ceil((listedAtMs + WATCHLIST_PERIOD_DAYS * DAY_MS - now) / DAY_MS))
|
|
322
|
+
: WATCHLIST_PERIOD_DAYS,
|
|
231
323
|
reason: `零拦截:${s.evaluated} 次评估从未 fail`,
|
|
232
324
|
});
|
|
233
325
|
}
|
|
234
326
|
}
|
|
235
327
|
|
|
236
|
-
return candidates;
|
|
328
|
+
return { candidates, watchlist: watchlistEntries, nextWatchlist };
|
|
237
329
|
}
|
|
238
330
|
|
|
239
331
|
/**
|
|
240
|
-
*
|
|
332
|
+
* 退役候选诊断(纯函数)——观察名单兼容包装
|
|
333
|
+
*
|
|
334
|
+
* 每条约束最多归入一个类别,优先级:
|
|
335
|
+
* zero_trigger → unevaluable → high_noise → zero_intercept
|
|
336
|
+
*
|
|
337
|
+
* 只返回退役候选;观察名单分组与待写回状态走 `buildConstraintsUsageReport`
|
|
338
|
+
* 的 `watchlist` / `nextWatchlist` 字段。zero_intercept 仅在观察期满且样本足时
|
|
339
|
+
* 才出现在本返回值里(ADR-0032),未期满的命中不再直接列候选。
|
|
340
|
+
*/
|
|
341
|
+
export function diagnoseRetireCandidates(
|
|
342
|
+
stats: ConstraintUsageStats[],
|
|
343
|
+
thresholds: Partial<DiagnoseThresholds> = {},
|
|
344
|
+
watch: DiagnoseWatchOptions = {}
|
|
345
|
+
): RetireCandidate[] {
|
|
346
|
+
const t = { ...DEFAULT_DIAGNOSE_THRESHOLDS, ...thresholds };
|
|
347
|
+
return diagnose(stats, t, watch).candidates;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 构建 report 数据模型(只读;观察名单状态以快照入、以 nextWatchlist 出,不写盘)
|
|
241
352
|
*/
|
|
242
353
|
export function buildConstraintsUsageReport(
|
|
243
354
|
projectRoot: string = process.cwd(),
|
|
244
|
-
thresholds: Partial<DiagnoseThresholds> = {}
|
|
355
|
+
thresholds: Partial<DiagnoseThresholds> = {},
|
|
356
|
+
watch: DiagnoseWatchOptions = {}
|
|
245
357
|
): ConstraintsUsageReport {
|
|
246
358
|
const effective = getEffectiveConstraints(projectRoot);
|
|
247
359
|
|
|
@@ -250,12 +362,15 @@ export function buildConstraintsUsageReport(
|
|
|
250
362
|
const usage = collectUsageByConstraint(traces);
|
|
251
363
|
|
|
252
364
|
const stats = effective.map(c => toStats(c.id, c.severity, usage.get(c.id)));
|
|
253
|
-
const
|
|
365
|
+
const t = { ...DEFAULT_DIAGNOSE_THRESHOLDS, ...thresholds };
|
|
366
|
+
const outcome = diagnose(stats, t, watch);
|
|
254
367
|
const lint = lintEffectiveConfig(projectRoot);
|
|
255
368
|
|
|
256
369
|
return {
|
|
257
370
|
stats,
|
|
258
|
-
candidates,
|
|
371
|
+
candidates: outcome.candidates,
|
|
372
|
+
watchlist: outcome.watchlist,
|
|
373
|
+
nextWatchlist: outcome.nextWatchlist,
|
|
259
374
|
lint,
|
|
260
375
|
traceFileExists: fs.existsSync(tracePath),
|
|
261
376
|
skippedLines,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 项目配置加载器
|
|
3
3
|
*
|
|
4
|
-
* 解读 `.harness/config.yml
|
|
5
|
-
*
|
|
6
|
-
* ADR-0023 决策 2
|
|
4
|
+
* 解读 `.harness/config.yml`、合并内置与应用层(`.harness/constraints.yml`,ADR-0033)约束。
|
|
5
|
+
* 本模块不碰文件系统:config.yml 的读取口径 = 运行级观察面(`constraints/run-env.ts`,
|
|
6
|
+
* ADR-0023 决策 2),一次运行内至多读一次;constraints.yml 的读取收在
|
|
7
|
+
* `app-constraints-loader.ts`(memo 口径与 rawConfig 同形)。
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import type { Constraint } from '../types/constraint';
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
import { CONSTRAINTS } from './constraints/definitions';
|
|
17
18
|
import { PRESETS_BY_NAME, STANDARD_PRESET } from '../presets';
|
|
18
19
|
import { filterEnabledEntries } from './effective-set';
|
|
20
|
+
import { loadAppConstraints } from './app-constraints-loader';
|
|
19
21
|
import { resolveRunEnv, type RunEnv, type RunTarget } from './constraints/run-env';
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -131,12 +133,14 @@ export class ProjectConfigLoader {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
/**
|
|
134
|
-
* 合并内置约束(生效集完整链路,ADR-0029)
|
|
136
|
+
* 合并内置约束(生效集完整链路,ADR-0029 + ADR-0033)
|
|
135
137
|
*
|
|
136
|
-
* 合并顺序:内置 → preset 裁剪 →
|
|
138
|
+
* 合并顺序:内置 → preset 裁剪 → 并入应用层(`.harness/constraints.yml`)→
|
|
139
|
+
* config.yml `constraints.<id>.enabled:false` 删除(对内置与应用层同口径生效,
|
|
140
|
+
* retired 墓碑随 enabled:false 天然生效)。
|
|
137
141
|
*
|
|
138
142
|
* config.yml 中未知约束 id(如已移除约束的禁用残留)静默忽略,
|
|
139
|
-
* 记录在结果 unknownIds
|
|
143
|
+
* 记录在结果 unknownIds 中供诊断;应用层 id 是已知 id,不进 unknownIds。
|
|
140
144
|
*
|
|
141
145
|
* @param options.preset 覆盖 config.yml 的 preset(CLI --preset 用);
|
|
142
146
|
* 未知预设名回落 standard + stderr 警告
|
|
@@ -184,9 +188,16 @@ export class ProjectConfigLoader {
|
|
|
184
188
|
unknownIds,
|
|
185
189
|
};
|
|
186
190
|
|
|
187
|
-
// 1.
|
|
191
|
+
// 1. 并入应用层约束(.harness/constraints.yml,ADR-0033):先进桶再走 config.yml
|
|
192
|
+
// 筛选,enabled:false/retired 墓碑对应用层 id 才能同口径生效
|
|
193
|
+
const appConstraints = loadAppConstraints(this.env);
|
|
194
|
+
for (const c of appConstraints) {
|
|
195
|
+
result.constraints[c.id] = c;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. 处理启用/禁用配置(未知 id 静默忽略,记录供诊断;应用层 id 属已知 id)
|
|
188
199
|
if (this.config.constraints) {
|
|
189
|
-
const knownIds = new Set(Object.keys(CONSTRAINTS));
|
|
200
|
+
const knownIds = new Set([...Object.keys(CONSTRAINTS), ...appConstraints.map(c => c.id)]);
|
|
190
201
|
const filtered = filterEnabledEntries(knownIds, this.config.constraints);
|
|
191
202
|
unknownIds.push(...filtered.unknownIds);
|
|
192
203
|
for (const constraintId of filtered.disabledIds) {
|
|
@@ -196,4 +196,58 @@ describe('CommandGate', () => {
|
|
|
196
196
|
expect(r.passed).toBe(true);
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
+
|
|
200
|
+
describe('git destructive rules(ADR-0031,票08 P1-6)', () => {
|
|
201
|
+
let gate: CommandGate;
|
|
202
|
+
beforeEach(() => { gate = new CommandGate(); });
|
|
203
|
+
|
|
204
|
+
it('should block git push --force', async () => {
|
|
205
|
+
const r = await gate.check('git push --force origin master');
|
|
206
|
+
expect(r.passed).toBe(false);
|
|
207
|
+
expect(r.details?.blocked.some((b: any) => b.id === 'git-push-force')).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should block git push -f and --force-with-lease', async () => {
|
|
211
|
+
expect((await gate.check('git push -f origin master')).passed).toBe(false);
|
|
212
|
+
expect((await gate.check('git push --force-with-lease')).passed).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should block git clean -f / -fd / -fdx', async () => {
|
|
216
|
+
for (const cmd of ['git clean -f', 'git clean -fd', 'git clean -fdx']) {
|
|
217
|
+
const r = await gate.check(cmd);
|
|
218
|
+
expect(r.passed).toBe(false);
|
|
219
|
+
expect(r.details?.blocked.some((b: any) => b.id === 'git-clean-force')).toBe(true);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should warn on git reset --hard / checkout -- / branch -D / filter-branch / reflog expire', async () => {
|
|
224
|
+
const cases: Array<[string, string]> = [
|
|
225
|
+
['git reset --hard HEAD~3', 'git-reset-hard'],
|
|
226
|
+
['git checkout -- src/foo.ts', 'git-checkout-discard'],
|
|
227
|
+
['git branch -D feature-x', 'git-branch-force-delete'],
|
|
228
|
+
['git filter-branch --tree-filter x', 'git-history-rewrite'],
|
|
229
|
+
['git reflog expire --expire=now --all', 'git-reflog-expire'],
|
|
230
|
+
];
|
|
231
|
+
for (const [cmd, id] of cases) {
|
|
232
|
+
const r = await gate.check(cmd);
|
|
233
|
+
expect(r.details?.warnings.some((w: any) => w.id === id)).toBe(true);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should allow safe git commands', async () => {
|
|
238
|
+
for (const cmd of [
|
|
239
|
+
'git status',
|
|
240
|
+
'git push origin master',
|
|
241
|
+
'git reset --soft HEAD~1',
|
|
242
|
+
'git clean -n',
|
|
243
|
+
'git checkout -b feature',
|
|
244
|
+
'git branch -d feature-x',
|
|
245
|
+
'git commit -m "msg"',
|
|
246
|
+
]) {
|
|
247
|
+
const r = await gate.check(cmd);
|
|
248
|
+
expect(r.passed).toBe(true);
|
|
249
|
+
expect(r.details?.warnings ?? []).toEqual([]);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
199
253
|
});
|
package/src/gates/command.ts
CHANGED
|
@@ -192,6 +192,57 @@ export const DEFAULT_COMMAND_BLACKLIST: CommandBlacklistRule[] = [
|
|
|
192
192
|
message: '卸载 Python 包需要确认',
|
|
193
193
|
category: 'package',
|
|
194
194
|
},
|
|
195
|
+
|
|
196
|
+
// ========== git 破坏类(ADR-0031,wayfinder 票08 P1-6:只补不可逆损失面) ==========
|
|
197
|
+
{
|
|
198
|
+
id: 'git-push-force',
|
|
199
|
+
pattern: /\bgit\s+push\b[^|;&]*(--force\b|-f\b)/i,
|
|
200
|
+
level: 'block',
|
|
201
|
+
message: '禁止强推远程(覆盖共享历史不可逆)',
|
|
202
|
+
category: 'git',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'git-clean-force',
|
|
206
|
+
pattern: /\bgit\s+clean\b[^|;&]*-[a-z]*f/i,
|
|
207
|
+
level: 'block',
|
|
208
|
+
message: '禁止 git clean -f(未跟踪文件删除不可恢复)',
|
|
209
|
+
category: 'git',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'git-reset-hard',
|
|
213
|
+
pattern: /\bgit\s+reset\s+--hard\b/i,
|
|
214
|
+
level: 'warn',
|
|
215
|
+
message: 'git reset --hard 丢弃未提交改动,需要确认',
|
|
216
|
+
category: 'git',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'git-checkout-discard',
|
|
220
|
+
pattern: /\bgit\s+checkout\s+--\s/i,
|
|
221
|
+
level: 'warn',
|
|
222
|
+
message: 'git checkout -- 丢弃工作区改动,需要确认',
|
|
223
|
+
category: 'git',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'git-branch-force-delete',
|
|
227
|
+
pattern: /\bgit\s+branch\s+-D\b/,
|
|
228
|
+
level: 'warn',
|
|
229
|
+
message: 'git branch -D 强制删分支(未合并提交可能丢失),需要确认',
|
|
230
|
+
category: 'git',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: 'git-history-rewrite',
|
|
234
|
+
pattern: /\bgit\s+(filter-branch|filter-repo)\b/i,
|
|
235
|
+
level: 'warn',
|
|
236
|
+
message: '历史重写操作不可逆,需要确认',
|
|
237
|
+
category: 'git',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: 'git-reflog-expire',
|
|
241
|
+
pattern: /\bgit\s+reflog\s+expire\b/i,
|
|
242
|
+
level: 'warn',
|
|
243
|
+
message: 'reflog expire 拆除恢复网,需要确认',
|
|
244
|
+
category: 'git',
|
|
245
|
+
},
|
|
195
246
|
];
|
|
196
247
|
|
|
197
248
|
/**
|
package/src/knowledge/CONTEXT.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
## 核心导出
|
|
20
20
|
- `KnowledgeStore`(类型面)/ `FileKnowledgeStore`(`store.ts`,barrel 与包根交出的**值**符号——store 的文件系统实现,构造收 `{ baseDir }`;仓内构造点两处:`cli/commands/knowledge-view.ts` 的 `openKnowledgeStore()`(knowledge 各子命令的取数口)与 `cli/commands/constraints-retire.ts` 的退役沉淀写入)— 知识条目 CRUD + 结构化存储;`saveAll()` 全量批量写入(循环内只更新内存索引、结束一次 writeIndex,harness#107)、`applyAll()` 按 id 部分更新的批量出口(`update(id, partial)` 的批量形,语义含未知 id 逐条跳过与空批零读写,harness#134)、`getConsumptionStats()` 读 `.consumption-stats.json` 的当日消费计数(打分核心唯一的消费统计取数口,harness#134)。**写入闸(E1 复盘修正 M1)**:save/saveAll/applyAll 落盘前校验 maturity/layer 枚举(update 经 save 继承),未声明值拒写抛错不静默降级;值域正本 = `types.ts` 的 `MATURITY_LEVELS`/`STORAGE_LAYERS` 常量(不进 barrel)。批量路径先整批预校验再落盘(任一脏值全批拒写);applyAll 批内同 id 多条按序累积合并语义不变
|
|
21
21
|
- `KnowledgeQuery` — 语义搜索 + 类型/标签过滤 + `queryByMode()` + `consume(taskContext)`;`query()` 是 budget 截断管线(仅用于 prompt 注入),`search()` 是全语料文本搜索(先匹配→排序→limit,交互搜索专用,勿用 query() 代替)
|
|
22
|
-
- `KnowledgeLifecycle` — per-mode 生命周期管理;缺省阈值常量 `DEFAULT_DECAY_CONFIG`(`provenDecayMonths: 12` / `verifiedDecayMonths: 6` / `draftDecayMonths: 3` / `autoPromoteSources: []`,正本 `types.ts`)经构造器 `{...DEFAULT_DECAY_CONFIG, ...config}` 合并,barrel
|
|
22
|
+
- `KnowledgeLifecycle` — per-mode 生命周期管理;缺省阈值常量 `DEFAULT_DECAY_CONFIG`(`provenDecayMonths: 12` / `verifiedDecayMonths: 6` / `draftDecayMonths: 3` / `autoPromoteSources: []`,正本 `types.ts`)经构造器 `{...DEFAULT_DECAY_CONFIG, ...config}` 合并,barrel 与包根一并交出;候选判定两族同位(tryPromote 升 proven 后自动触发):`checkSkillCandidate`(AC-8a)与 `checkConstraintCandidate`(块 3 子项 6,ADR-0033——口径 = proven + 未打标 + referencedBy≥5 + contributors≥3 个不同来源会话,打 `constraintCandidate` 标 + `constraintCandidate:marked` 事件,与 skillCandidate 互不干扰可同挂)
|
|
23
23
|
- `extractCodeStructure(dir)`(`primitives/code-structure.ts`,配 `CodeStructure`/`DeclarationInfo`/`ImportInfo` 三类型对外)— 目录代码结构提取;仓内零调用方,是留给下游的公开面
|
|
24
24
|
- `KnowledgeIngest` — 知识摄取引擎(含 ingest gate 质量门)+ `ingestExternal()` 外部内容摄入
|
|
25
25
|
- `sanitizeExternalContent` — 外部内容安全清洗(过滤注入模式 + 长度限制)
|
|
@@ -59,5 +59,7 @@
|
|
|
59
59
|
- 约束"退役不删除"——retire 落盘 config.yml `enabled: false` + retired 元数据,保留规则原文 + 退役原因 + 历史统计(可回滚)
|
|
60
60
|
- `MaturityLevel` / `StorageLayer` 的值域不抄清单——运行时正本是 `types.ts` 的 `MATURITY_LEVELS` / `STORAGE_LAYERS` 常量(写入闸与 lint 枚举校验共用)
|
|
61
61
|
- `excludeArchived` 同时排除 archived 和 deprecated
|
|
62
|
+
- `list()` 的过滤层是 `index.json`(tags/types 匹配都打在 IndexEntry 上),返回体才回读 .md 全文——手工清洗/直改 .md 数据区时须同步 index(`rebuildIndex()` 或同改 index.json),否则过滤口径与文件内容漂移(#614 存量 tags 清洗同步了两边)
|
|
63
|
+
- tag 写入纪律(#614 审计):凡 append tag 处先判存在或走 Set 去重——`audit.ts`/`ingest.ts` 的 low_quality 有 includes 闸、ingest 合并走 `new Set`、`lifecycle.checkSkillCandidate`/`checkConstraintCandidate` 由前置谓词挡住重复;studio 侧 rule-scanner/pattern-miner 曾因缺闸造成实盘 tags 千次重复腐蚀
|
|
62
64
|
- 仍有两处按**原始** `referencedBy.length` 判定,不属飞轮指标、ADR-0013 明确列为范围外:`lifecycle.ts` 的 signal 饱和 / reference 激活(退役阈值调整另票)、`knowledge health` 的 D1「verified 零引用」线索提示(逐条 issue 线索,非聚合分子)
|
|
63
65
|
- #134 收口后仍在的逐条写点(同型问题,本票裁决点名的循环之外,需要时另票):`lint.ts` 的三处 autoFix `store.update()`、`lifecycle.recordReference()`(逐事件调用,返回更新后条目并触发回调,批量化会改它的契约)、`ingest.mergeEntries()`;`store.list()` 每条一次 `findFile()` 线性扫索引的 O(N²) 按 #134 裁决**未动**,待把知识树实际规模重新量一次再判是否单开票
|
|
@@ -956,4 +956,85 @@ describe('KnowledgeLifecycle', () => {
|
|
|
956
956
|
expect(lifecycle.checkEntryDecay('SIG-001')).toBeUndefined();
|
|
957
957
|
});
|
|
958
958
|
});
|
|
959
|
+
|
|
960
|
+
describe('checkConstraintCandidate(块 3 子项 6,ADR-0033)', () => {
|
|
961
|
+
/** 达标基线:proven + 5 条引用 + 3 个不同来源会话 */
|
|
962
|
+
const eligible = (overrides?: Partial<KnowledgeEntry>): KnowledgeEntry => makeEntry({
|
|
963
|
+
maturity: 'proven',
|
|
964
|
+
referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04', 'b:2026-05-05'],
|
|
965
|
+
contributors: ['a', 'b', 'c'],
|
|
966
|
+
...overrides,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it('达标:打 constraintCandidate 标 + 触发 constraintCandidate:marked 事件,返回 true', () => {
|
|
970
|
+
const events: Array<{ entryId: string; context?: string }> = [];
|
|
971
|
+
lifecycle.onReference(e => events.push({ entryId: e.entryId, context: e.context }));
|
|
972
|
+
store.save(eligible());
|
|
973
|
+
|
|
974
|
+
expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(true);
|
|
975
|
+
expect(store.get('DEC-001')!.tags).toContain('constraintCandidate');
|
|
976
|
+
expect(events).toEqual([{ entryId: 'DEC-001', context: 'constraintCandidate:marked' }]);
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('不达标不打标:非 proven / 引用不足 5 / 来源会话不足 3', () => {
|
|
980
|
+
store.save(eligible({ id: 'E-1', maturity: 'verified' }));
|
|
981
|
+
store.save(eligible({ id: 'E-2', referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04'] }));
|
|
982
|
+
store.save(eligible({ id: 'E-3', contributors: ['a', 'b'] }));
|
|
983
|
+
|
|
984
|
+
for (const id of ['E-1', 'E-2', 'E-3']) {
|
|
985
|
+
expect(lifecycle.checkConstraintCandidate(id)).toBe(false);
|
|
986
|
+
expect(store.get(id)!.tags).not.toContain('constraintCandidate');
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
it('幂等:已打标不重复打、不重复触发事件', () => {
|
|
991
|
+
const events: string[] = [];
|
|
992
|
+
lifecycle.onReference(e => { if (e.context) events.push(e.context); });
|
|
993
|
+
store.save(eligible({ tags: ['constraintCandidate'] }));
|
|
994
|
+
|
|
995
|
+
expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(false);
|
|
996
|
+
expect(store.get('DEC-001')!.tags.filter(t => t === 'constraintCandidate')).toHaveLength(1);
|
|
997
|
+
expect(events).toEqual([]);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('不存在的条目返回 false', () => {
|
|
1001
|
+
expect(lifecycle.checkConstraintCandidate('NON-EXISTENT')).toBe(false);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
it('与 skillCandidate 互不干扰:达标条目可同时挂两标', () => {
|
|
1005
|
+
store.save(eligible({
|
|
1006
|
+
content: 'a'.repeat(200),
|
|
1007
|
+
executionResults: [
|
|
1008
|
+
{ contributor: 'a', success: true, timestamp: '2026-05-01' },
|
|
1009
|
+
{ contributor: 'b', success: true, timestamp: '2026-05-02' },
|
|
1010
|
+
{ contributor: 'c', success: true, timestamp: '2026-05-03' },
|
|
1011
|
+
{ contributor: 'a', success: true, timestamp: '2026-05-04' },
|
|
1012
|
+
{ contributor: 'b', success: true, timestamp: '2026-05-05' },
|
|
1013
|
+
],
|
|
1014
|
+
}));
|
|
1015
|
+
|
|
1016
|
+
expect(lifecycle.checkSkillCandidate('DEC-001')).toBe(true);
|
|
1017
|
+
expect(lifecycle.checkConstraintCandidate('DEC-001')).toBe(true);
|
|
1018
|
+
const tags = store.get('DEC-001')!.tags;
|
|
1019
|
+
expect(tags).toContain('skillCandidate');
|
|
1020
|
+
expect(tags).toContain('constraintCandidate');
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('tryPromote 升到 proven 后自动触发判定(与 checkSkillCandidate 同位)', () => {
|
|
1024
|
+
const events: string[] = [];
|
|
1025
|
+
lifecycle.onReference(e => { if (e.context) events.push(e.context); });
|
|
1026
|
+
store.save(makeEntry({
|
|
1027
|
+
maturity: 'verified',
|
|
1028
|
+
content: 'a'.repeat(100),
|
|
1029
|
+
contributors: ['a', 'b', 'c'],
|
|
1030
|
+
projects: ['p1', 'p2'],
|
|
1031
|
+
referencedBy: ['a:2026-05-01', 'b:2026-05-02', 'c:2026-05-03', 'a:2026-05-04', 'b:2026-05-05'],
|
|
1032
|
+
}));
|
|
1033
|
+
|
|
1034
|
+
const change = lifecycle.tryPromote('DEC-001');
|
|
1035
|
+
expect(change?.to).toBe('proven');
|
|
1036
|
+
expect(store.get('DEC-001')!.tags).toContain('constraintCandidate');
|
|
1037
|
+
expect(events).toContain('constraintCandidate:marked');
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
959
1040
|
});
|
|
@@ -199,8 +199,10 @@ export class KnowledgeLifecycle {
|
|
|
199
199
|
this.store.update(entryId, { maturity: target });
|
|
200
200
|
|
|
201
201
|
// AC-8a: After promotion, check skillCandidate eligibility
|
|
202
|
+
// 块 3 子项 6(ADR-0033):同位触发 constraintCandidate 判定——两标独立,可同挂
|
|
202
203
|
if (target === 'proven') {
|
|
203
204
|
this.checkSkillCandidate(entryId);
|
|
205
|
+
this.checkConstraintCandidate(entryId);
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
return change;
|
|
@@ -253,6 +255,53 @@ export class KnowledgeLifecycle {
|
|
|
253
255
|
return true;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
/**
|
|
259
|
+
* 块 3 子项 6(ADR-0033):Check if a proven entry qualifies as a constraintCandidate.
|
|
260
|
+
* Marks with 'constraintCandidate' tag and emits event if eligible.
|
|
261
|
+
* Returns true if newly marked.
|
|
262
|
+
*
|
|
263
|
+
* 判定口径(人类已批准,设计稿 Q4):proven + 未打过标 + references≥5
|
|
264
|
+
* (referencedBy 条数,与 recordReference 的 refKey 口径一致)+ ≥3 个不同来源会话
|
|
265
|
+
* (contributors,与 skill 候选同字段)。与 skillCandidate 互不干扰,可同挂两标。
|
|
266
|
+
*/
|
|
267
|
+
checkConstraintCandidate(entryId: string): boolean {
|
|
268
|
+
const entry = this.store.get(entryId);
|
|
269
|
+
if (!entry) return false;
|
|
270
|
+
if (!this.isConstraintCandidate(entry)) return false;
|
|
271
|
+
|
|
272
|
+
// Mark as constraintCandidate
|
|
273
|
+
this.store.update(entryId, {
|
|
274
|
+
tags: [...entry.tags, 'constraintCandidate'],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Emit event for downstream consumers
|
|
278
|
+
const now = new Date().toISOString();
|
|
279
|
+
for (const cb of this.onReferenceCallbacks) {
|
|
280
|
+
try {
|
|
281
|
+
cb({
|
|
282
|
+
entryId,
|
|
283
|
+
contributor: 'lifecycle',
|
|
284
|
+
timestamp: now,
|
|
285
|
+
context: 'constraintCandidate:marked',
|
|
286
|
+
});
|
|
287
|
+
} catch { /* non-blocking */ }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 块 3 子项 6:Check if an entry meets constraintCandidate criteria.
|
|
295
|
+
*/
|
|
296
|
+
private isConstraintCandidate(entry: KnowledgeEntry): boolean {
|
|
297
|
+
if (entry.maturity !== 'proven') return false;
|
|
298
|
+
if (entry.tags.includes('constraintCandidate')) return false;
|
|
299
|
+
if (entry.referencedBy.length < 5) return false;
|
|
300
|
+
if (entry.contributors.length < 3) return false;
|
|
301
|
+
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
256
305
|
/**
|
|
257
306
|
* AC-8c: Remove skillCandidate tag if success rate drops below 50%.
|
|
258
307
|
* Called after execution writeback.
|
|
@@ -32,16 +32,17 @@ describe('预设数据', () => {
|
|
|
32
32
|
describe('RELAXED_PRESET', () => {
|
|
33
33
|
it('应该只启用核心约束', () => {
|
|
34
34
|
expect(RELAXED_PRESET.name).toBe('relaxed');
|
|
35
|
-
expect(RELAXED_PRESET.errors).toHaveLength(
|
|
36
|
-
expect(RELAXED_PRESET.warnings).toHaveLength(
|
|
35
|
+
expect(RELAXED_PRESET.errors).toHaveLength(2);
|
|
36
|
+
expect(RELAXED_PRESET.warnings).toHaveLength(0);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('应该包含 no_completion_without_verification', () => {
|
|
40
40
|
expect(RELAXED_PRESET.errors).toContain('no_completion_without_verification');
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
it('应该包含 no_hardcoded_credentials', () => {
|
|
44
|
-
expect(RELAXED_PRESET.
|
|
43
|
+
it('应该包含 no_hardcoded_credentials(ADR-0032 起为 error 级,同桶)', () => {
|
|
44
|
+
expect(RELAXED_PRESET.errors).toContain('no_hardcoded_credentials');
|
|
45
|
+
expect(RELAXED_PRESET.warnings).toEqual([]);
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
|
package/src/presets/standard.ts
CHANGED
|
@@ -47,16 +47,15 @@ export const STANDARD_PRESET: PresetConfig = {
|
|
|
47
47
|
/**
|
|
48
48
|
* 宽松预设
|
|
49
49
|
*
|
|
50
|
-
* 仅启用核心 error
|
|
50
|
+
* 仅启用核心 error 级与凭证扫描(ADR-0032 起凭证扫描为 error 级,同桶)
|
|
51
51
|
*/
|
|
52
52
|
export const RELAXED_PRESET: PresetConfig = {
|
|
53
53
|
name: 'relaxed',
|
|
54
54
|
errors: [
|
|
55
55
|
'no_completion_without_verification',
|
|
56
|
-
],
|
|
57
|
-
warnings: [
|
|
58
56
|
'no_hardcoded_credentials',
|
|
59
57
|
],
|
|
58
|
+
warnings: [],
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
/**
|
package/src/types/constraint.ts
CHANGED
|
@@ -32,6 +32,15 @@ export type ConstraintSeverity = 'error' | 'warning' | 'info';
|
|
|
32
32
|
*/
|
|
33
33
|
export type ConstraintTrigger = string;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 约束来源(ADR-0033 两层约束模型)
|
|
37
|
+
*
|
|
38
|
+
* - builtin:内置约束(definitions/ 字面量定义,checker 按约束 id 注册查找)
|
|
39
|
+
* - app:应用层约束(应用仓 `.harness/constraints.yml` 定义,checker 按模板 id +
|
|
40
|
+
* params 实例化);缺省 builtin
|
|
41
|
+
*/
|
|
42
|
+
export type ConstraintSource = 'builtin' | 'app';
|
|
43
|
+
|
|
35
44
|
/**
|
|
36
45
|
* 约束定义
|
|
37
46
|
*/
|
|
@@ -65,6 +74,25 @@ export interface Constraint {
|
|
|
65
74
|
|
|
66
75
|
/** 是否启用 */
|
|
67
76
|
enabled?: boolean;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 约束来源(ADR-0033):缺省 builtin
|
|
80
|
+
*
|
|
81
|
+
* app = 应用层约束(应用仓 `.harness/constraints.yml` 定义);
|
|
82
|
+
* report/退休/提案据此区分来源
|
|
83
|
+
*/
|
|
84
|
+
source?: ConstraintSource;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 填空式 checker 模板 id(仅 source='app' 使用)
|
|
88
|
+
*
|
|
89
|
+
* 内置约束省略 = checker 按约束 id 在内置注册表查找;应用层约束按模板 id
|
|
90
|
+
* 在模板注册表(checkers/index.ts TEMPLATES)查找并带 params 实例化
|
|
91
|
+
*/
|
|
92
|
+
checker?: string;
|
|
93
|
+
|
|
94
|
+
/** 模板参数(仅 source='app' 使用,形状由各模板的 validateParams 校验) */
|
|
95
|
+
params?: Record<string, unknown>;
|
|
68
96
|
}
|
|
69
97
|
|
|
70
98
|
/**
|