@dommaker/harness 1.10.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 +31 -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 +36 -11
- 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/commands/scaffold-templates.d.ts +1 -1
- package/dist/cli/commands/scaffold-templates.d.ts.map +1 -1
- package/dist/cli/commands/scaffold-templates.js +0 -16
- package/dist/cli/commands/scaffold-templates.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 +59 -7
- package/dist/core/constraints/usage-report.d.ts.map +1 -1
- package/dist/core/constraints/usage-report.js +62 -13
- 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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -2
- package/dist/index.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/knowledge/lint.d.ts +17 -0
- package/dist/knowledge/lint.d.ts.map +1 -1
- package/dist/knowledge/lint.js +52 -0
- package/dist/knowledge/lint.js.map +1 -1
- package/dist/knowledge/store.d.ts +7 -0
- package/dist/knowledge/store.d.ts.map +1 -1
- package/dist/knowledge/store.js +48 -13
- package/dist/knowledge/store.js.map +1 -1
- package/dist/knowledge/types.d.ts +5 -1
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +5 -1
- package/dist/knowledge/types.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/__tests__/public-exports.test.ts +7 -0
- package/src/__tests__/public-type-surface.test.ts +5 -0
- package/src/__tests__/usage-report-public-export.test.ts +21 -0
- package/src/cli/commands/CONTEXT.md +5 -4
- 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-json-flag.test.ts +101 -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__/init-ondisk.test.ts +1 -1
- package/src/cli/commands/__tests__/init.test.ts +1 -5
- package/src/cli/commands/__tests__/knowledge.test.ts +6 -16
- package/src/cli/commands/__tests__/numeric-flag-assembly.test.ts +1 -1
- package/src/cli/commands/__tests__/passes-gate-dead-options.test.ts +1 -1
- package/src/cli/commands/__tests__/registry.test.ts +1 -1
- 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 +36 -11
- package/src/cli/commands/knowledge-view.ts +9 -28
- package/src/cli/commands/report.ts +9 -5
- package/src/cli/commands/scaffold-templates.ts +0 -16
- 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 +129 -14
- 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/index.ts +20 -0
- package/src/knowledge/CONTEXT.md +7 -4
- package/src/knowledge/__tests__/lifecycle.test.ts +81 -0
- package/src/knowledge/__tests__/lint.test.ts +84 -0
- package/src/knowledge/__tests__/store.test.ts +70 -0
- package/src/knowledge/lifecycle.ts +49 -0
- package/src/knowledge/lint.ts +60 -1
- package/src/knowledge/store.ts +48 -11
- package/src/knowledge/types.ts +7 -1
- package/src/monitoring/CONTEXT.md +1 -1
- package/src/presets/__tests__/standard.test.ts +5 -4
- package/src/presets/standard.ts +2 -3
- package/src/types/constraint.ts +28 -0
- package/dist/cli/commands/posteval-plan.d.ts +0 -15
- package/dist/cli/commands/posteval-plan.d.ts.map +0 -1
- package/dist/cli/commands/posteval-plan.js +0 -86
- package/dist/cli/commands/posteval-plan.js.map +0 -1
- package/src/cli/commands/__tests__/posteval-plan.test.ts +0 -204
- package/src/cli/commands/posteval-plan.ts +0 -98
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
buildConstraintsUsageReport,
|
|
16
16
|
diagnoseRetireCandidates,
|
|
17
17
|
readProjectTraces,
|
|
18
|
+
WATCHLIST_PERIOD_DAYS,
|
|
18
19
|
} from '../../../core/constraints/usage-report';
|
|
20
|
+
import type { HarnessState, StateIO } from '../../state-io';
|
|
19
21
|
import { constraintsReport, renderExportMarkdown } from '../constraints-report';
|
|
20
22
|
import { createProjectFixture, writeProjectTraces } from '../../../test-setup/project-fixture';
|
|
21
23
|
|
|
@@ -126,13 +128,13 @@ describe('buildConstraintsUsageReport', () => {
|
|
|
126
128
|
expect(noiseC?.kind).toBe('high_noise');
|
|
127
129
|
expect(noiseC?.reason).toContain('83%');
|
|
128
130
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
expect(
|
|
131
|
+
// 零拦截:先进观察名单(ADR-0032 中间态),不直接进退役候选
|
|
132
|
+
expect(byId.get('no_hardcoded_credentials')).toBeUndefined();
|
|
133
|
+
const watched = report.watchlist.find(w => w.id === 'no_hardcoded_credentials');
|
|
134
|
+
expect(watched?.reason).toContain('50 次评估');
|
|
133
135
|
});
|
|
134
136
|
|
|
135
|
-
it('阈值可配:放宽零拦截样本阈值后 10 次全 pass
|
|
137
|
+
it('阈值可配:放宽零拦截样本阈值后 10 次全 pass 进观察名单(不直接进候选)', () => {
|
|
136
138
|
const root = createProjectFixture({
|
|
137
139
|
name: 'harness-report-test',
|
|
138
140
|
traces: tracesOf('no_hardcoded_credentials', 'pass', 10),
|
|
@@ -140,9 +142,11 @@ describe('buildConstraintsUsageReport', () => {
|
|
|
140
142
|
|
|
141
143
|
const strict = buildConstraintsUsageReport(root);
|
|
142
144
|
expect(strict.candidates.find(c => c.id === 'no_hardcoded_credentials')).toBeUndefined();
|
|
145
|
+
expect(strict.watchlist.find(w => w.id === 'no_hardcoded_credentials')).toBeUndefined();
|
|
143
146
|
|
|
144
147
|
const relaxed = buildConstraintsUsageReport(root, { zeroInterceptMinEvaluated: 10 });
|
|
145
|
-
expect(relaxed.candidates.find(c => c.id === 'no_hardcoded_credentials')
|
|
148
|
+
expect(relaxed.candidates.find(c => c.id === 'no_hardcoded_credentials')).toBeUndefined();
|
|
149
|
+
expect(relaxed.watchlist.find(w => w.id === 'no_hardcoded_credentials')).toBeDefined();
|
|
146
150
|
});
|
|
147
151
|
|
|
148
152
|
it('diagnoseRetireCandidates 优先级:全 skip 不重复计入零拦截', () => {
|
|
@@ -286,3 +290,168 @@ describe('constraintsReport 坏行数透传(harness#100)', () => {
|
|
|
286
290
|
expect(fs.readFileSync(path.join(clean, 'report.md'), 'utf-8')).not.toContain('损坏');
|
|
287
291
|
});
|
|
288
292
|
});
|
|
293
|
+
|
|
294
|
+
describe('零拦截观察名单(ADR-0032,块 3 子项 4)', () => {
|
|
295
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
296
|
+
// 固定时钟:观察期判定全靠注入 now,不在测试里等 90 天
|
|
297
|
+
const NOW = 1700100000000;
|
|
298
|
+
const NOW_ISO = new Date(NOW).toISOString();
|
|
299
|
+
|
|
300
|
+
/** 内存 StateIO 假件(与 status/check 测试同形) */
|
|
301
|
+
function memoryStateIO(initial: HarnessState = {}): StateIO & { snapshot(): HarnessState } {
|
|
302
|
+
let state = initial;
|
|
303
|
+
return {
|
|
304
|
+
read: () => state,
|
|
305
|
+
write: (s: HarnessState) => { state = s; },
|
|
306
|
+
snapshot: () => state,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** 50 次全 pass 的零拦截 fixture 项目 */
|
|
311
|
+
function zeroInterceptProject(id = 'no_hardcoded_credentials', n = 50): string {
|
|
312
|
+
return createProjectFixture({
|
|
313
|
+
name: 'harness-watchlist-test',
|
|
314
|
+
traces: tracesOf(id, 'pass', n, NOW - 10 * DAY_MS),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
it('命中且不在名单:列入观察名单(记列入时间),进 watchlist 分组不进退役候选', () => {
|
|
319
|
+
const root = zeroInterceptProject();
|
|
320
|
+
const report = buildConstraintsUsageReport(root, {}, { now: NOW });
|
|
321
|
+
|
|
322
|
+
expect(report.candidates.some(c => c.id === 'no_hardcoded_credentials')).toBe(false);
|
|
323
|
+
const w = report.watchlist.find(x => x.id === 'no_hardcoded_credentials');
|
|
324
|
+
expect(w).toBeDefined();
|
|
325
|
+
expect(w!.listedAt).toBe(NOW_ISO);
|
|
326
|
+
expect(w!.remainingDays).toBe(WATCHLIST_PERIOD_DAYS);
|
|
327
|
+
expect(w!.reason).toContain('50 次评估');
|
|
328
|
+
// 待写回状态只增不删
|
|
329
|
+
expect(report.nextWatchlist['no_hardcoded_credentials']).toEqual({ listedAt: NOW_ISO });
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('观察期未满:仍留名单不转候选,剩余观察期按天折算', () => {
|
|
333
|
+
const root = zeroInterceptProject();
|
|
334
|
+
const listedAt = new Date(NOW - 30 * DAY_MS).toISOString();
|
|
335
|
+
const report = buildConstraintsUsageReport(root, {}, {
|
|
336
|
+
watchlist: { no_hardcoded_credentials: { listedAt } },
|
|
337
|
+
now: NOW,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(report.candidates.some(c => c.id === 'no_hardcoded_credentials')).toBe(false);
|
|
341
|
+
const w = report.watchlist.find(x => x.id === 'no_hardcoded_credentials');
|
|
342
|
+
expect(w!.listedAt).toBe(listedAt);
|
|
343
|
+
expect(w!.remainingDays).toBe(WATCHLIST_PERIOD_DAYS - 30);
|
|
344
|
+
// 已列入不重复写
|
|
345
|
+
expect(report.nextWatchlist).toEqual({ no_hardcoded_credentials: { listedAt } });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('期满且样本够:转正式 zero_intercept 退役候选', () => {
|
|
349
|
+
const root = zeroInterceptProject();
|
|
350
|
+
const listedAt = new Date(NOW - WATCHLIST_PERIOD_DAYS * DAY_MS).toISOString();
|
|
351
|
+
const report = buildConstraintsUsageReport(root, {}, {
|
|
352
|
+
watchlist: { no_hardcoded_credentials: { listedAt } },
|
|
353
|
+
now: NOW,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const c = report.candidates.find(x => x.id === 'no_hardcoded_credentials');
|
|
357
|
+
expect(c?.kind).toBe('zero_intercept');
|
|
358
|
+
expect(c?.reason).toContain('观察期满');
|
|
359
|
+
expect(report.watchlist.some(w => w.id === 'no_hardcoded_credentials')).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('期满但样本不够(评估数跌破阈值):不转候选', () => {
|
|
363
|
+
const root = zeroInterceptProject('no_hardcoded_credentials', 30);
|
|
364
|
+
const listedAt = new Date(NOW - 120 * DAY_MS).toISOString();
|
|
365
|
+
const report = buildConstraintsUsageReport(root, {}, {
|
|
366
|
+
watchlist: { no_hardcoded_credentials: { listedAt } },
|
|
367
|
+
now: NOW,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(report.candidates.some(c => c.id === 'no_hardcoded_credentials')).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('其他三类候选不进观察名单(zero_trigger/unevaluable/high_noise 维持现状)', () => {
|
|
374
|
+
const root = createProjectFixture({ name: 'harness-watchlist-test' });
|
|
375
|
+
writeProjectTraces(root, [
|
|
376
|
+
...tracesOf('capability_sync', 'skip', 5), // 不可评估
|
|
377
|
+
...tracesOf('no_test_simplification', 'fail', 20), // 高噪
|
|
378
|
+
...tracesOf('no_test_simplification', 'pass', 4, 1700001000000),
|
|
379
|
+
// docs_freshness 等零触发
|
|
380
|
+
]);
|
|
381
|
+
const report = buildConstraintsUsageReport(root, {}, { now: NOW });
|
|
382
|
+
|
|
383
|
+
expect(report.candidates.length).toBeGreaterThan(0);
|
|
384
|
+
expect(report.watchlist).toEqual([]);
|
|
385
|
+
expect(report.nextWatchlist).toEqual({});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('diagnoseRetireCandidates 兼容签名:只回候选,观察期内的零拦截不出现', () => {
|
|
389
|
+
const stats = [{
|
|
390
|
+
id: 'x', severity: 'warning' as const,
|
|
391
|
+
total: 50, pass: 50, fail: 0, skip: 0,
|
|
392
|
+
evaluated: 50, failRate: 0,
|
|
393
|
+
}];
|
|
394
|
+
expect(diagnoseRetireCandidates(stats)).toEqual([]);
|
|
395
|
+
const listedAt = new Date(NOW - 100 * DAY_MS).toISOString();
|
|
396
|
+
const converted = diagnoseRetireCandidates(stats, {}, {
|
|
397
|
+
watchlist: { x: { listedAt } },
|
|
398
|
+
now: NOW,
|
|
399
|
+
});
|
|
400
|
+
expect(converted.map(c => c.kind)).toEqual(['zero_intercept']);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('列入时间损坏按未列入处理:重新列入、时钟重启', () => {
|
|
404
|
+
const root = zeroInterceptProject();
|
|
405
|
+
const report = buildConstraintsUsageReport(root, {}, {
|
|
406
|
+
watchlist: { no_hardcoded_credentials: { listedAt: 'not-a-date' } },
|
|
407
|
+
now: NOW,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
expect(report.candidates.some(c => c.id === 'no_hardcoded_credentials')).toBe(false);
|
|
411
|
+
expect(report.nextWatchlist['no_hardcoded_credentials']).toEqual({ listedAt: NOW_ISO });
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('CLI 级:report 经 StateIO 持久化列入,第二次运行读回仍在名单', async () => {
|
|
415
|
+
const root = zeroInterceptProject();
|
|
416
|
+
const stateIO = memoryStateIO();
|
|
417
|
+
|
|
418
|
+
await constraintsReport({ projectPath: root, stateIO, now: NOW }, io);
|
|
419
|
+
expect(stateIO.snapshot().constraintWatchlist?.['no_hardcoded_credentials']).toEqual({ listedAt: NOW_ISO });
|
|
420
|
+
expect(io.outText()).toContain('观察名单');
|
|
421
|
+
expect(io.outText()).toContain('no_hardcoded_credentials');
|
|
422
|
+
|
|
423
|
+
// 第二次:名单状态经注入面读回,不重复写、不进候选
|
|
424
|
+
const io2 = captureIO();
|
|
425
|
+
await constraintsReport({ projectPath: root, stateIO, now: NOW + 10 * DAY_MS }, io2);
|
|
426
|
+
expect(io2.outText()).toContain('剩余观察 80 天');
|
|
427
|
+
expect(io2.outText()).not.toContain('[零拦截] no_hardcoded_credentials');
|
|
428
|
+
|
|
429
|
+
// 期满:同一条转退役候选
|
|
430
|
+
const io3 = captureIO();
|
|
431
|
+
await constraintsReport({ projectPath: root, stateIO, now: NOW + WATCHLIST_PERIOD_DAYS * DAY_MS }, io3);
|
|
432
|
+
expect(io3.outText()).toContain('[零拦截] no_hardcoded_credentials');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('StateIO 写回不动其他状态字段(读-改-写)', async () => {
|
|
436
|
+
const root = zeroInterceptProject();
|
|
437
|
+
const stateIO = memoryStateIO({ shownHints: ['trace_50'], lastStatusRun: '2026-09-01T00:00:00.000Z' });
|
|
438
|
+
|
|
439
|
+
await constraintsReport({ projectPath: root, stateIO, now: NOW }, captureIO());
|
|
440
|
+
|
|
441
|
+
const s = stateIO.snapshot();
|
|
442
|
+
expect(s.shownHints).toEqual(['trace_50']);
|
|
443
|
+
expect(s.lastStatusRun).toBe('2026-09-01T00:00:00.000Z');
|
|
444
|
+
expect(Object.keys(s.constraintWatchlist ?? {})).toEqual(['no_hardcoded_credentials']);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('--export 脱敏摘要带观察名单分组(列入时间 + 剩余观察期)', async () => {
|
|
448
|
+
const root = zeroInterceptProject();
|
|
449
|
+
await constraintsReport({ projectPath: root, export: 'report.md', now: NOW }, captureIO());
|
|
450
|
+
|
|
451
|
+
const md = fs.readFileSync(path.join(root, 'report.md'), 'utf-8');
|
|
452
|
+
expect(md).toContain('## 观察名单');
|
|
453
|
+
expect(md).toContain('no_hardcoded_credentials');
|
|
454
|
+
expect(md).toContain(`剩余观察 ${WATCHLIST_PERIOD_DAYS} 天`);
|
|
455
|
+
expect(md).not.toContain(root);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import { captureIO, type CapturingIO } from '../../command-contract';
|
|
17
|
+
import * as os from 'os';
|
|
17
18
|
import * as path from 'path';
|
|
18
19
|
import { PassThrough, Writable } from 'stream';
|
|
19
20
|
import * as yaml from 'js-yaml';
|
|
@@ -21,6 +22,7 @@ import { getConstraint } from '../../../core/constraints/definitions';
|
|
|
21
22
|
import { getEffectiveConstraints } from '../../../core/effective-constraints';
|
|
22
23
|
import { FileKnowledgeStore } from '../../../knowledge/store';
|
|
23
24
|
import { retireConstraint, constraintsRetire, runRetireInteractive, printRetireResult } from '../constraints-retire';
|
|
25
|
+
import { openKnowledgeStore } from '../knowledge-view';
|
|
24
26
|
import { createProjectFixture, writeProjectTraces } from '../../../test-setup/project-fixture';
|
|
25
27
|
|
|
26
28
|
const FIXED_NOW = new Date('2026-08-08T12:00:00.000Z');
|
|
@@ -32,6 +34,12 @@ function readConfig(root: string): any {
|
|
|
32
34
|
let io: CapturingIO;
|
|
33
35
|
beforeEach(() => {
|
|
34
36
|
io = captureIO();
|
|
37
|
+
// retire 的 KnowledgeStore 写口走统一解析点(harness#177):不隔离会写进真实用户主目录
|
|
38
|
+
process.env.KNOWLEDGE_BASE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-retire-kb-'));
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
fs.rmSync(process.env.KNOWLEDGE_BASE_DIR!, { recursive: true, force: true });
|
|
42
|
+
delete process.env.KNOWLEDGE_BASE_DIR;
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
describe('retireConstraint 执行逻辑', () => {
|
|
@@ -49,7 +57,7 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
expect(result.status).toBe('retired');
|
|
52
|
-
expect(result.isError).toBe(
|
|
60
|
+
expect(result.isError).toBe(true);
|
|
53
61
|
expect(result.stats).toEqual({ total: 3, fail: 1, failRate: 1 / 3 });
|
|
54
62
|
|
|
55
63
|
const config = readConfig(root);
|
|
@@ -77,7 +85,7 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
77
85
|
expect(config.constraints.capability_sync.retired.reason).toBe('');
|
|
78
86
|
});
|
|
79
87
|
|
|
80
|
-
it('KnowledgeStore 写入退役记录:规则原文 + 原因 + 统计 + signal
|
|
88
|
+
it('KnowledgeStore 写入退役记录:规则原文 + 原因 + 统计 + signal 模式,落点为 knowledge 读口同一解析根(harness#177)', () => {
|
|
81
89
|
const root = createProjectFixture({
|
|
82
90
|
name: 'harness-retire-test',
|
|
83
91
|
traces: [{ constraintId: 'no_completion_without_verification', result: 'fail' }],
|
|
@@ -86,8 +94,12 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
86
94
|
const result = retireConstraint(root, 'no_completion_without_verification', { reason: '流程已内置门禁', now: FIXED_NOW });
|
|
87
95
|
expect(result.status).toBe('retired');
|
|
88
96
|
expect(result.knowledgeEntryId).toBe('constraint-retired-no_completion_without_verification');
|
|
97
|
+
// 写口与读口同一解析点:KNOWLEDGE_BASE_DIR 覆盖对写口同步生效
|
|
98
|
+
expect(result.knowledgeBaseDir).toBe(process.env.KNOWLEDGE_BASE_DIR);
|
|
99
|
+
// 缺省解析(无 -p/--dir)构造的 store 直接可读——修复前写口硬编码 projectRoot,此处读不到
|
|
100
|
+
expect(openKnowledgeStore({}, io).get('constraint-retired-no_completion_without_verification')).toBeDefined();
|
|
89
101
|
|
|
90
|
-
const store = new FileKnowledgeStore({ baseDir:
|
|
102
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
91
103
|
const entry = store.get('constraint-retired-no_completion_without_verification');
|
|
92
104
|
expect(entry).toBeDefined();
|
|
93
105
|
expect(entry!.consumptionMode).toBe('signal');
|
|
@@ -104,7 +116,7 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
104
116
|
expect(entry!.content).toContain(FIXED_NOW.toISOString());
|
|
105
117
|
});
|
|
106
118
|
|
|
107
|
-
it('退役结果打印含回滚语义提示(恢复 =
|
|
119
|
+
it('退役结果打印含回滚语义提示(恢复 = constraints reactivate 命令)', () => {
|
|
108
120
|
const root = createProjectFixture({ name: 'harness-retire-test' });
|
|
109
121
|
const result = retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
110
122
|
expect(result.status).toBe('retired');
|
|
@@ -112,7 +124,38 @@ describe('retireConstraint 执行逻辑', () => {
|
|
|
112
124
|
printRetireResult(result, io);
|
|
113
125
|
const output = io.outText();
|
|
114
126
|
expect(output).toContain('恢复方法');
|
|
115
|
-
expect(output).toContain('constraints
|
|
127
|
+
expect(output).toContain('harness constraints reactivate capability_sync');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('退役结果打印:git 仓内附 commit 提示,非 git 目录不提示(票 02 断点 4)', () => {
|
|
131
|
+
const gitRoot = createProjectFixture({ name: 'harness-retire-test', files: { '.git/HEAD': 'ref: refs/heads/master\n' } });
|
|
132
|
+
const inGit = retireConstraint(gitRoot, 'capability_sync', { now: FIXED_NOW });
|
|
133
|
+
printRetireResult(inGit, io, gitRoot);
|
|
134
|
+
expect(io.outText()).toContain('git add .harness/config.yml');
|
|
135
|
+
|
|
136
|
+
const plainRoot = createProjectFixture({ name: 'harness-retire-test' });
|
|
137
|
+
const plainIo = captureIO();
|
|
138
|
+
const notInGit = retireConstraint(plainRoot, 'capability_sync', { now: FIXED_NOW });
|
|
139
|
+
printRetireResult(notInGit, plainIo, plainRoot);
|
|
140
|
+
expect(plainIo.outText()).not.toContain('git add');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('裸 disable 不吞退休:enabled:false 无墓碑时 retire 照常落墓碑 + 沉淀(ADR-0032 决策 6.6,票 02 断点 6)', () => {
|
|
144
|
+
const root = createProjectFixture({
|
|
145
|
+
name: 'harness-retire-test',
|
|
146
|
+
config: 'constraints:\n capability_sync:\n enabled: false\n',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const result = retireConstraint(root, 'capability_sync', { reason: '升级裸 disable 为退休', now: FIXED_NOW });
|
|
150
|
+
|
|
151
|
+
expect(result.status).toBe('retired');
|
|
152
|
+
const config = readConfig(root);
|
|
153
|
+
expect(config.constraints.capability_sync.enabled).toBe(false);
|
|
154
|
+
expect(config.constraints.capability_sync.retired.at).toBe(FIXED_NOW.toISOString());
|
|
155
|
+
expect(config.constraints.capability_sync.retired.reason).toBe('升级裸 disable 为退休');
|
|
156
|
+
// 沉淀照写,不被裸 disable 吞掉
|
|
157
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
158
|
+
expect(store.get('constraint-retired-capability_sync')).toBeDefined();
|
|
116
159
|
});
|
|
117
160
|
|
|
118
161
|
it('重复 retire:already_retired,不覆盖原 retired 元数据', () => {
|
|
@@ -229,14 +272,14 @@ describe('constraintsRetire 非交互直达', () => {
|
|
|
229
272
|
|
|
230
273
|
it('--yes 直达 warning 级退役:不打印 error 级警示并落盘', async () => {
|
|
231
274
|
const root = createProjectFixture({ name: 'harness-retire-test' });
|
|
232
|
-
await constraintsRetire('
|
|
275
|
+
await constraintsRetire('capability_sync', { projectPath: root, reason: '直接退役', yes: true }, io);
|
|
233
276
|
|
|
234
277
|
const output = io.outText();
|
|
235
278
|
expect(output).not.toContain('是一条 error 级约束');
|
|
236
279
|
expect(output).toContain('已退役');
|
|
237
280
|
|
|
238
281
|
const config = readConfig(root);
|
|
239
|
-
expect(config.constraints.
|
|
282
|
+
expect(config.constraints.capability_sync.enabled).toBe(false);
|
|
240
283
|
});
|
|
241
284
|
|
|
242
285
|
it('--yes 直达未知 id:明确提示', async () => {
|
|
@@ -305,7 +348,7 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
|
|
|
305
348
|
]);
|
|
306
349
|
writeProjectTraces(root, healthy);
|
|
307
350
|
|
|
308
|
-
const streams = makeIo(['
|
|
351
|
+
const streams = makeIo(['capability_sync', '误报太多', 'y']);
|
|
309
352
|
const printed = captureLog();
|
|
310
353
|
await runRetireInteractive(root, streams);
|
|
311
354
|
streams.done();
|
|
@@ -316,8 +359,8 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
|
|
|
316
359
|
expect(output).toContain('已退役');
|
|
317
360
|
|
|
318
361
|
const config = readConfig(root);
|
|
319
|
-
expect(config.constraints.
|
|
320
|
-
expect(config.constraints.
|
|
362
|
+
expect(config.constraints.capability_sync.enabled).toBe(false);
|
|
363
|
+
expect(config.constraints.capability_sync.retired.reason).toBe('误报太多');
|
|
321
364
|
});
|
|
322
365
|
|
|
323
366
|
it('坏行 fixture:候选列表前告知损坏行数(决策依据不完整不静默,harness#100)', async () => {
|
|
@@ -403,3 +446,85 @@ describe('runRetireInteractive 交互流程(注入 IO 流)', () => {
|
|
|
403
446
|
expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
404
447
|
});
|
|
405
448
|
});
|
|
449
|
+
|
|
450
|
+
describe('应用层约束退休(ADR-0033 块 3 子项 3)', () => {
|
|
451
|
+
const APP_YML = [
|
|
452
|
+
'constraints:',
|
|
453
|
+
' - id: app_no_internal_url',
|
|
454
|
+
' rule: Web code must not contain internal URLs',
|
|
455
|
+
' checker: regex-scan',
|
|
456
|
+
' params:',
|
|
457
|
+
" pattern: 'https?://10\\.'",
|
|
458
|
+
' severity: warning',
|
|
459
|
+
' message: 检测到内网地址',
|
|
460
|
+
'',
|
|
461
|
+
].join('\n');
|
|
462
|
+
|
|
463
|
+
function appFixture(configYml?: string): string {
|
|
464
|
+
return createProjectFixture({
|
|
465
|
+
name: 'harness-retire-test',
|
|
466
|
+
config: configYml,
|
|
467
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
it('墓碑写 config.yml 与内置同形(enabled:false + retired),constraints.yml 条文保留不删', () => {
|
|
472
|
+
const root = appFixture();
|
|
473
|
+
const ymlBefore = fs.readFileSync(path.join(root, '.harness', 'constraints.yml'), 'utf-8');
|
|
474
|
+
|
|
475
|
+
const result = retireConstraint(root, 'app_no_internal_url', { reason: '网关统一拦截', now: FIXED_NOW });
|
|
476
|
+
|
|
477
|
+
expect(result.status).toBe('retired');
|
|
478
|
+
expect(result.isError).toBe(false); // 应用层 severity=warning
|
|
479
|
+
const config = readConfig(root);
|
|
480
|
+
const entry = config.constraints.app_no_internal_url;
|
|
481
|
+
expect(entry.enabled).toBe(false);
|
|
482
|
+
expect(entry.retired.at).toBe(FIXED_NOW.toISOString());
|
|
483
|
+
expect(entry.retired.reason).toBe('网关统一拦截');
|
|
484
|
+
// 退休=停用不是删除:定义正本逐字节不动
|
|
485
|
+
expect(fs.readFileSync(path.join(root, '.harness', 'constraints.yml'), 'utf-8')).toBe(ymlBefore);
|
|
486
|
+
// 生效集排除
|
|
487
|
+
expect(getEffectiveConstraints(root).some(c => c.id === 'app_no_internal_url')).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('沉淀条目复用 saveRetireKnowledge,tags 加 source:app', () => {
|
|
491
|
+
const root = appFixture();
|
|
492
|
+
const result = retireConstraint(root, 'app_no_internal_url', { now: FIXED_NOW });
|
|
493
|
+
|
|
494
|
+
expect(result.status).toBe('retired');
|
|
495
|
+
expect(result.knowledgeEntryId).toBe('constraint-retired-app_no_internal_url');
|
|
496
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
497
|
+
const entry = store.get('constraint-retired-app_no_internal_url');
|
|
498
|
+
expect(entry).toBeDefined();
|
|
499
|
+
expect(entry!.tags).toContain('constraint-retired');
|
|
500
|
+
expect(entry!.tags).toContain('constraint:app_no_internal_url');
|
|
501
|
+
expect(entry!.tags).toContain('source:app');
|
|
502
|
+
expect(entry!.content).toContain('Web code must not contain internal URLs');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('内置退休沉淀不带 source:app(tags 形状不漂移)', () => {
|
|
506
|
+
const root = createProjectFixture({ name: 'harness-retire-test' });
|
|
507
|
+
retireConstraint(root, 'capability_sync', { now: FIXED_NOW });
|
|
508
|
+
const store = new FileKnowledgeStore({ baseDir: process.env.KNOWLEDGE_BASE_DIR! });
|
|
509
|
+
expect(store.get('constraint-retired-capability_sync')!.tags).not.toContain('source:app');
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
it('重复退休应用层:already_retired 幂等,不覆盖原墓碑', () => {
|
|
513
|
+
const root = appFixture();
|
|
514
|
+
retireConstraint(root, 'app_no_internal_url', { reason: '第一次', now: FIXED_NOW });
|
|
515
|
+
|
|
516
|
+
const second = retireConstraint(root, 'app_no_internal_url', {
|
|
517
|
+
reason: '第二次',
|
|
518
|
+
now: new Date('2026-08-09T00:00:00.000Z'),
|
|
519
|
+
});
|
|
520
|
+
expect(second.status).toBe('already_retired');
|
|
521
|
+
expect(readConfig(root).constraints.app_no_internal_url.retired.reason).toBe('第一次');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('constraints.yml 里的 id 与内置都找不到 → unknown_id', () => {
|
|
525
|
+
const root = appFixture();
|
|
526
|
+
const result = retireConstraint(root, 'app_ghost', { now: FIXED_NOW });
|
|
527
|
+
expect(result.status).toBe('unknown_id');
|
|
528
|
+
expect(fs.existsSync(path.join(root, '.harness', 'config.yml'))).toBe(false);
|
|
529
|
+
});
|
|
530
|
+
});
|
|
@@ -156,7 +156,7 @@ describe('init 真落盘(无 IO mock)', () => {
|
|
|
156
156
|
const hook = await read('.git/hooks/pre-commit');
|
|
157
157
|
expect(hook.startsWith('#!/bin/sh\n# Harness pre-commit hook\n')).toBe(true);
|
|
158
158
|
expect(hook).toContain('echo "🔍 Running harness checks..."');
|
|
159
|
-
expect(hook).toContain(
|
|
159
|
+
expect(hook).toContain('npx @dommaker/harness check --staged');
|
|
160
160
|
expect(hook.endsWith('echo "✅ All checks passed"\n')).toBe(true);
|
|
161
161
|
|
|
162
162
|
// 第 9 站点(#144):字节级冻结,改模板必须同步改这份基线(同 GITLAB_PLAIN 手法)
|
|
@@ -105,8 +105,7 @@ describe('init command', () => {
|
|
|
105
105
|
it('应该输出代码片段模式', async () => {
|
|
106
106
|
await init({ preset: 'standard', printSnippets: true }, io);
|
|
107
107
|
expect(io.outText()).not.toBe('');
|
|
108
|
-
|
|
109
|
-
expect(io.outText()).toContain("grep -E 'plans/.*\\.md$|\\.plan\\.md$'");
|
|
108
|
+
expect(io.outText()).toContain('npx @dommaker/harness check --staged');
|
|
110
109
|
});
|
|
111
110
|
});
|
|
112
111
|
|
|
@@ -153,10 +152,7 @@ describe('init command', () => {
|
|
|
153
152
|
const hookCall = writeCalls.find((c: any[]) => String(c[0]).includes('pre-commit'));
|
|
154
153
|
expect(hookCall).toBeDefined();
|
|
155
154
|
expect(hookCall![1]).toContain('npx @dommaker/harness check --staged');
|
|
156
|
-
expect(hookCall![1]).toContain('npx @dommaker/harness posteval-plan');
|
|
157
155
|
expect(hookCall![1]).not.toMatch(/npx harness /);
|
|
158
|
-
// plan 匹配模式须为转义点(#35:模板字面量 \. 运行期退化为 .)
|
|
159
|
-
expect(hookCall![1]).toContain("grep -E 'plans/.*\\.md$|\\.plan\\.md$'");
|
|
160
156
|
});
|
|
161
157
|
|
|
162
158
|
it('pre-commit 片段与落盘 hook 同源(#103:差异行并入共享片段,无第二份文本)', async () => {
|
|
@@ -85,7 +85,8 @@ beforeEach(() => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
// 解析函数本体自 #133 起住 knowledge-view.ts(resolveKnowledgeBaseDir),行为面不变:
|
|
88
|
-
// 这里经 knowledgeStats 观测 `-p` / KNOWLEDGE_BASE_DIR /
|
|
88
|
+
// 这里经 knowledgeStats 观测 `-p` / KNOWLEDGE_BASE_DIR / 缺省目录三条路径的解析结果
|
|
89
|
+
// (ADR-0034:旧 ~/.studio/knowledge 兼容沿用已退役,不再属观测面)
|
|
89
90
|
describe('getKnowledgeDir', () => {
|
|
90
91
|
const fs = require('fs');
|
|
91
92
|
const os = require('os');
|
|
@@ -119,34 +120,23 @@ describe('getKnowledgeDir', () => {
|
|
|
119
120
|
}));
|
|
120
121
|
});
|
|
121
122
|
|
|
122
|
-
it('should default to ~/.harness/knowledge when no env var
|
|
123
|
+
it('should default to ~/.harness/knowledge when no env var', async () => {
|
|
123
124
|
await knowledgeStats({ json: true }, io);
|
|
124
125
|
expect(storeCtorSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
125
126
|
baseDir: path.join(tmpHome, '.harness', 'knowledge'),
|
|
126
127
|
}));
|
|
127
128
|
});
|
|
128
129
|
|
|
129
|
-
it('should
|
|
130
|
+
it('should NOT fall back to legacy ~/.studio/knowledge even when it has data (ADR-0034 目录收编:兼容沿用已退役)', async () => {
|
|
130
131
|
const legacyDir = path.join(tmpHome, '.studio', 'knowledge');
|
|
131
132
|
fs.mkdirSync(legacyDir, { recursive: true });
|
|
132
133
|
fs.writeFileSync(path.join(legacyDir, 'guideline-x.md'), 'data');
|
|
133
134
|
await knowledgeStats({ json: true }, io);
|
|
134
|
-
|
|
135
|
-
baseDir: legacyDir,
|
|
136
|
-
}));
|
|
137
|
-
expect(io.errText()).toContain('旧目录');
|
|
138
|
-
// 闩锁:同进程重复解析只提示一次
|
|
139
|
-
await knowledgeStats({ json: true }, io);
|
|
140
|
-
const notices = io.errLines().filter(line => line.includes('旧目录'));
|
|
141
|
-
expect(notices).toHaveLength(1);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('should ignore legacy dir when it exists but is empty', async () => {
|
|
145
|
-
fs.mkdirSync(path.join(tmpHome, '.studio', 'knowledge'), { recursive: true });
|
|
146
|
-
await knowledgeStats({ json: true }, io);
|
|
135
|
+
// 旧目录有数据也不再沿用:落缺省 ~/.harness/knowledge,无兼容提示
|
|
147
136
|
expect(storeCtorSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
148
137
|
baseDir: path.join(tmpHome, '.harness', 'knowledge'),
|
|
149
138
|
}));
|
|
139
|
+
expect(io.errText()).not.toContain('旧目录');
|
|
150
140
|
});
|
|
151
141
|
|
|
152
142
|
it('should use projectPath when provided', async () => {
|
|
@@ -57,7 +57,7 @@ const performanceCli = GATE_DEFINITIONS.find(g => g.id === 'performance')!.cli;
|
|
|
57
57
|
const reviewCli = GATE_DEFINITIONS.find(g => g.id === 'review')!.cli;
|
|
58
58
|
|
|
59
59
|
describe('constraints report 三阈值:装配点 fail-loud', () => {
|
|
60
|
-
const valid = {
|
|
60
|
+
const valid = { jsonOutput: false, zeroInterceptMin: '50', noiseFailRate: '0.8', noiseMinTotal: '20' };
|
|
61
61
|
|
|
62
62
|
it('缺省字符串 → 数值(int/int/float 各归其形)', () => {
|
|
63
63
|
expect(reportDef.mapActionArgs!([], valid)).toEqual([{
|
|
@@ -85,7 +85,7 @@ describe('passes-gate 重试选项已删除(A1)', () => {
|
|
|
85
85
|
it('采集面确实覆盖三类命令位(前提,防空扫致闸失效)', () => {
|
|
86
86
|
const rendered = ALL_CLI_SITES.map(s => `${s.slot} ${s.token}`);
|
|
87
87
|
expect(rendered).toContain('passes-gate --coverage-threshold'); // 通用表叶子命令的旗帜
|
|
88
|
-
expect(rendered).toContain('constraints report --json'); // children 递归(嵌套命令位)
|
|
88
|
+
expect(rendered).toContain('constraints report --json-output'); // children 递归(嵌套命令位)
|
|
89
89
|
expect(rendered).toContain('acceptance --task-id'); // 门禁表命令位(GATE_DEFINITIONS.cli)
|
|
90
90
|
expect(rendered).toContain('check list'); // 选项路由键(commander camelCase 键位)
|
|
91
91
|
});
|
|
@@ -35,7 +35,7 @@ function collectRefs(defs: CommandDefinition[]): CommandImplRef[] {
|
|
|
35
35
|
|
|
36
36
|
const EXPECTED_TOP_LEVEL_COMMANDS = [
|
|
37
37
|
'check', 'validate', 'passes-gate', 'init', 'report', 'status', 'spec',
|
|
38
|
-
'sync-docs', 'knowledge', 'sdd', 'failure',
|
|
38
|
+
'sync-docs', 'knowledge', 'sdd', 'failure',
|
|
39
39
|
'release', 'constraints',
|
|
40
40
|
'spec-baseline-check',
|
|
41
41
|
// 6 门禁命令(GATE_DEFINITIONS 驱动)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* report 命令生效集口径(ADR-0033 块 3 子项 3)
|
|
3
|
+
*
|
|
4
|
+
* 缺口修复回归:report 此前跑内置全集(CONSTRAINTS),config.yml 禁用与
|
|
5
|
+
* 应用层约束(.harness/constraints.yml)都进不了报告面。修复后 collectConstraints
|
|
6
|
+
* 传 merged config(与 check.ts 同一 getMergedConstraintsConfig 入口),
|
|
7
|
+
* 应用层条目进总数、违规如实上报。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from '@jest/globals';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { report } from '../report';
|
|
13
|
+
import { captureIO } from '../../command-contract';
|
|
14
|
+
import { CONSTRAINTS } from '../../../core/constraints/definitions';
|
|
15
|
+
import { createProjectFixture } from '../../../test-setup/project-fixture';
|
|
16
|
+
|
|
17
|
+
const BUILTIN_TOTAL = Object.keys(CONSTRAINTS).length;
|
|
18
|
+
|
|
19
|
+
/** 应用层约束:file-exists 指向一个不存在的文件(warning 级,必违规) */
|
|
20
|
+
const APP_YML = [
|
|
21
|
+
'constraints:',
|
|
22
|
+
' - id: app_required_readme',
|
|
23
|
+
' rule: Project must keep README.app.md',
|
|
24
|
+
' checker: file-exists',
|
|
25
|
+
' params:',
|
|
26
|
+
' path: README.app.md',
|
|
27
|
+
' severity: warning',
|
|
28
|
+
' message: 缺少 README.app.md',
|
|
29
|
+
'',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
function parseJsonReport(output: string) {
|
|
33
|
+
const start = output.indexOf('{');
|
|
34
|
+
const end = output.lastIndexOf('}');
|
|
35
|
+
return JSON.parse(output.slice(start, end + 1));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('report 生效集口径(含应用层)', () => {
|
|
39
|
+
it('应用层约束进报告:总数 +1,违规进 violations', async () => {
|
|
40
|
+
const root = createProjectFixture({
|
|
41
|
+
name: 'report-app-effective',
|
|
42
|
+
files: { [path.join('.harness', 'constraints.yml')]: APP_YML },
|
|
43
|
+
});
|
|
44
|
+
const io = captureIO();
|
|
45
|
+
|
|
46
|
+
await report({ format: 'json', projectPath: root }, io);
|
|
47
|
+
|
|
48
|
+
const data = parseJsonReport(io.outText());
|
|
49
|
+
expect(data.constraints.total).toBe(BUILTIN_TOTAL + 1);
|
|
50
|
+
const ids = data.constraints.violations.map((v: { id: string }) => v.id);
|
|
51
|
+
expect(ids).toContain('app_required_readme');
|
|
52
|
+
expect(data.constraints.warningViolations).toBeGreaterThanOrEqual(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('config.yml 禁用内置约束后总数随生效集收窄(不再报内置全集)', async () => {
|
|
56
|
+
const root = createProjectFixture({
|
|
57
|
+
name: 'report-app-effective',
|
|
58
|
+
config: 'constraints:\n capability_sync:\n enabled: false\n',
|
|
59
|
+
});
|
|
60
|
+
const io = captureIO();
|
|
61
|
+
|
|
62
|
+
await report({ format: 'json', projectPath: root }, io);
|
|
63
|
+
|
|
64
|
+
const data = parseJsonReport(io.outText());
|
|
65
|
+
expect(data.constraints.total).toBe(BUILTIN_TOTAL - 1);
|
|
66
|
+
});
|
|
67
|
+
});
|