@dommaker/harness 0.18.0 → 1.0.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 +23 -0
- package/bin/harness.js +136 -552
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +2 -13
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/definitions.d.ts +90 -0
- package/dist/cli/commands/definitions.d.ts.map +1 -0
- package/dist/cli/commands/definitions.js +519 -0
- package/dist/cli/commands/definitions.js.map +1 -0
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts +3 -25
- package/dist/cli/commands/sync-docs/capabilities-syncer.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/capabilities-syncer.js +3 -93
- package/dist/cli/commands/sync-docs/capabilities-syncer.js.map +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts +1 -1
- package/dist/cli/commands/sync-docs/index.d.ts.map +1 -1
- package/dist/cli/commands/sync-docs/index.js +4 -4
- package/dist/cli/commands/sync-docs/index.js.map +1 -1
- package/dist/cli/commands/update-user-model.d.ts +1 -0
- package/dist/cli/commands/update-user-model.d.ts.map +1 -1
- package/dist/cli/commands/update-user-model.js +12 -5
- package/dist/cli/commands/update-user-model.js.map +1 -1
- package/dist/context/index.d.ts +1 -5
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +1 -5
- package/dist/context/index.js.map +1 -1
- package/dist/context/knowledge-injector.d.ts +3 -3
- package/dist/context/knowledge-injector.d.ts.map +1 -1
- package/dist/context/knowledge-injector.js +1 -1
- package/dist/context/session-manager.d.ts +1 -12
- package/dist/context/session-manager.d.ts.map +1 -1
- package/dist/context/session-manager.js +0 -63
- package/dist/context/session-manager.js.map +1 -1
- package/dist/context/types.d.ts +1 -39
- package/dist/context/types.d.ts.map +1 -1
- package/dist/context/types.js +2 -15
- package/dist/context/types.js.map +1 -1
- package/dist/core/constraints/agent-prompt-renderer.d.ts +32 -0
- package/dist/core/constraints/agent-prompt-renderer.d.ts.map +1 -0
- package/dist/core/constraints/agent-prompt-renderer.js +58 -0
- package/dist/core/constraints/agent-prompt-renderer.js.map +1 -0
- package/dist/core/constraints/capabilities-parser.d.ts +17 -2
- package/dist/core/constraints/capabilities-parser.d.ts.map +1 -1
- package/dist/core/constraints/capabilities-parser.js +84 -2
- package/dist/core/constraints/capabilities-parser.js.map +1 -1
- package/dist/core/constraints/check-cache.d.ts +48 -3
- package/dist/core/constraints/check-cache.d.ts.map +1 -1
- package/dist/core/constraints/check-cache.js +53 -5
- package/dist/core/constraints/check-cache.js.map +1 -1
- package/dist/core/constraints/checker.d.ts +3 -17
- package/dist/core/constraints/checker.d.ts.map +1 -1
- package/dist/core/constraints/checker.js +7 -47
- package/dist/core/constraints/checker.js.map +1 -1
- package/dist/core/constraints/definitions.d.ts +0 -7
- package/dist/core/constraints/definitions.d.ts.map +1 -1
- package/dist/core/constraints/definitions.js +1 -8
- package/dist/core/constraints/definitions.js.map +1 -1
- package/dist/core/constraints/doc-freshness/runner.d.ts +0 -2
- package/dist/core/constraints/doc-freshness/runner.d.ts.map +1 -1
- package/dist/core/constraints/doc-freshness/runner.js.map +1 -1
- package/dist/core/constraints/index.d.ts +5 -1
- package/dist/core/constraints/index.d.ts.map +1 -1
- package/dist/core/constraints/index.js +8 -3
- package/dist/core/constraints/index.js.map +1 -1
- package/dist/core/constraints/interceptor.d.ts.map +1 -1
- package/dist/core/constraints/interceptor.js +1 -5
- package/dist/core/constraints/interceptor.js.map +1 -1
- package/dist/core/effective-constraints.d.ts.map +1 -1
- package/dist/core/effective-constraints.js +0 -1
- package/dist/core/effective-constraints.js.map +1 -1
- package/dist/core/project-config-loader.d.ts.map +1 -1
- package/dist/core/project-config-loader.js +1 -8
- package/dist/core/project-config-loader.js.map +1 -1
- package/dist/dashboard/stats.d.ts.map +1 -1
- package/dist/dashboard/stats.js.map +1 -1
- package/dist/dashboard/types.d.ts +2 -2
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/failure/constraint-handler.d.ts.map +1 -1
- package/dist/failure/constraint-handler.js +0 -2
- package/dist/failure/constraint-handler.js.map +1 -1
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.d.ts.map +1 -1
- package/dist/gates/acceptance.js +21 -0
- package/dist/gates/acceptance.js.map +1 -1
- package/dist/gates/checker-gate.d.ts +23 -0
- package/dist/gates/checker-gate.d.ts.map +1 -0
- package/dist/gates/checker-gate.js +53 -0
- package/dist/gates/checker-gate.js.map +1 -0
- package/dist/gates/command.d.ts +9 -2
- package/dist/gates/command.d.ts.map +1 -1
- package/dist/gates/command.js +10 -0
- package/dist/gates/command.js.map +1 -1
- package/dist/gates/contract.d.ts +8 -2
- package/dist/gates/contract.d.ts.map +1 -1
- package/dist/gates/contract.js +9 -0
- package/dist/gates/contract.js.map +1 -1
- package/dist/gates/decision.d.ts +17 -0
- package/dist/gates/decision.d.ts.map +1 -0
- package/dist/gates/decision.js +25 -0
- package/dist/gates/decision.js.map +1 -0
- package/dist/gates/definitions.d.ts +63 -0
- package/dist/gates/definitions.d.ts.map +1 -0
- package/dist/gates/definitions.js +160 -0
- package/dist/gates/definitions.js.map +1 -0
- package/dist/gates/effective-gates.d.ts +30 -0
- package/dist/gates/effective-gates.d.ts.map +1 -0
- package/dist/gates/effective-gates.js +85 -0
- package/dist/gates/effective-gates.js.map +1 -0
- package/dist/gates/index.d.ts +25 -1
- package/dist/gates/index.d.ts.map +1 -1
- package/dist/gates/index.js +37 -1
- package/dist/gates/index.js.map +1 -1
- package/dist/gates/performance.d.ts +8 -2
- package/dist/gates/performance.d.ts.map +1 -1
- package/dist/gates/performance.js +9 -0
- package/dist/gates/performance.js.map +1 -1
- package/dist/gates/registry.d.ts +32 -0
- package/dist/gates/registry.d.ts.map +1 -0
- package/dist/gates/registry.js +90 -0
- package/dist/gates/registry.js.map +1 -0
- package/dist/gates/review.d.ts +8 -2
- package/dist/gates/review.d.ts.map +1 -1
- package/dist/gates/review.js +9 -0
- package/dist/gates/review.js.map +1 -1
- package/dist/gates/runner.d.ts +30 -0
- package/dist/gates/runner.d.ts.map +1 -0
- package/dist/gates/runner.js +38 -0
- package/dist/gates/runner.js.map +1 -0
- package/dist/gates/security.d.ts +8 -2
- package/dist/gates/security.d.ts.map +1 -1
- package/dist/gates/security.js +9 -0
- package/dist/gates/security.js.map +1 -1
- package/dist/gates/types.d.ts +41 -1
- package/dist/gates/types.d.ts.map +1 -1
- package/dist/hooks/bootstrap.d.ts.map +1 -1
- package/dist/hooks/bootstrap.js +0 -2
- package/dist/hooks/bootstrap.js.map +1 -1
- package/dist/hooks/config.d.ts +26 -0
- package/dist/hooks/config.d.ts.map +1 -0
- package/dist/hooks/config.js +30 -0
- package/dist/hooks/config.js.map +1 -0
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +4 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/registry.d.ts +18 -1
- package/dist/hooks/registry.d.ts.map +1 -1
- package/dist/hooks/registry.js +45 -0
- package/dist/hooks/registry.js.map +1 -1
- package/dist/hooks/types.d.ts +20 -0
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -21
- package/dist/index.js.map +1 -1
- package/dist/knowledge/import.d.ts +2 -2
- package/dist/knowledge/import.d.ts.map +1 -1
- package/dist/knowledge/ingest.js.map +1 -1
- package/dist/knowledge/lifecycle-hooks.d.ts +2 -2
- package/dist/knowledge/lifecycle-hooks.d.ts.map +1 -1
- package/dist/knowledge/types.d.ts +5 -7
- package/dist/knowledge/types.d.ts.map +1 -1
- package/dist/knowledge/types.js +1 -1
- package/dist/knowledge/types.js.map +1 -1
- package/dist/monitoring/constraint-doctor.d.ts +4 -22
- package/dist/monitoring/constraint-doctor.d.ts.map +1 -1
- package/dist/monitoring/constraint-doctor.js +7 -94
- package/dist/monitoring/constraint-doctor.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 +1 -2
- package/dist/presets/standard.js.map +1 -1
- package/dist/spec/annotation-checker.d.ts.map +1 -1
- package/dist/spec/annotation-checker.js +6 -26
- package/dist/spec/annotation-checker.js.map +1 -1
- package/dist/tools/index.d.ts +0 -4
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +0 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/types/constraint.d.ts +1 -12
- package/dist/types/constraint.d.ts.map +1 -1
- package/dist/types/constraint.js +0 -3
- package/dist/types/constraint.js.map +1 -1
- package/dist/types/project-config.d.ts +0 -2
- package/dist/types/project-config.d.ts.map +1 -1
- package/dist/utils/file-walk.d.ts +2 -2
- package/dist/utils/file-walk.js +2 -2
- package/package.json +9 -4
- package/src/CONTEXT.md +14 -0
- package/src/__tests__/checker-extra.test.ts +2 -59
- package/src/__tests__/constraint-doctor.test.ts +1 -140
- package/src/__tests__/constraint-handler.test.ts +0 -4
- package/src/__tests__/project-config-loader-extra.test.ts +0 -20
- package/src/__tests__/project-config-loader.test.ts +0 -1
- package/src/cli/commands/CONTEXT.md +15 -7
- package/src/cli/commands/__tests__/analyze-sessions.test.ts +178 -0
- package/src/cli/commands/__tests__/check-drift.test.ts +0 -3
- package/src/cli/commands/__tests__/check-skip-output.test.ts +0 -5
- package/src/cli/commands/__tests__/check.test.ts +7 -75
- package/src/cli/commands/__tests__/registry.test.ts +201 -0
- package/src/cli/commands/__tests__/release.test.ts +162 -0
- package/src/cli/commands/__tests__/update-user-model.test.ts +205 -0
- package/src/cli/commands/check.ts +2 -14
- package/src/cli/commands/definitions.ts +593 -0
- package/src/cli/commands/knowledge.ts +3 -3
- package/src/cli/commands/sync-docs/capabilities-syncer.ts +4 -110
- package/src/cli/commands/sync-docs/index.ts +8 -5
- package/src/cli/commands/update-user-model.ts +15 -5
- package/src/context/CONTEXT.md +2 -7
- package/src/context/__tests__/session-manager.test.ts +0 -91
- package/src/context/index.ts +1 -5
- package/src/context/knowledge-injector.ts +3 -3
- package/src/context/session-manager.ts +0 -74
- package/src/context/types.ts +2 -63
- package/src/core/CONTEXT.md +1 -1
- package/src/core/constraints/__tests__/agent-prompt-renderer.test.ts +129 -0
- package/src/core/constraints/__tests__/check-cache.test.ts +227 -0
- package/src/core/constraints/__tests__/interceptor.test.ts +0 -2
- package/src/core/constraints/agent-prompt-renderer.ts +71 -0
- package/src/core/constraints/capabilities-parser.ts +95 -2
- package/src/core/constraints/check-cache.ts +75 -3
- package/src/core/constraints/checker.ts +8 -53
- package/src/core/constraints/definitions.ts +0 -8
- package/src/core/constraints/doc-freshness/runner.ts +1 -2
- package/src/core/constraints/index.ts +11 -2
- package/src/core/constraints/interceptor.ts +1 -6
- package/src/core/effective-constraints.ts +0 -1
- package/src/core/project-config-loader.ts +1 -7
- package/src/dashboard/stats.ts +2 -2
- package/src/dashboard/types.ts +2 -2
- package/src/failure/constraint-handler.ts +0 -2
- package/src/gates/CONTEXT.md +16 -13
- package/src/gates/__tests__/checker-gate.test.ts +45 -0
- package/src/gates/__tests__/contract.test.ts +73 -0
- package/src/gates/__tests__/effective-gates.test.ts +82 -0
- package/src/gates/__tests__/gate-interface.test.ts +202 -0
- package/src/gates/__tests__/registry.test.ts +87 -0
- package/src/gates/__tests__/runner.test.ts +81 -0
- package/src/gates/acceptance.ts +24 -1
- package/src/gates/checker-gate.ts +54 -0
- package/src/gates/command.ts +13 -2
- package/src/gates/contract.ts +12 -2
- package/src/gates/decision.ts +27 -0
- package/src/gates/definitions.ts +207 -0
- package/src/gates/effective-gates.ts +106 -0
- package/src/gates/index.ts +38 -0
- package/src/gates/performance.ts +12 -2
- package/src/gates/registry.ts +107 -0
- package/src/gates/review.ts +12 -2
- package/src/gates/runner.ts +57 -0
- package/src/gates/security.ts +12 -2
- package/src/gates/types.ts +48 -1
- package/src/hooks/CONTEXT.md +12 -2
- package/src/hooks/__tests__/bootstrap.test.ts +0 -1
- package/src/hooks/__tests__/config.test.ts +19 -0
- package/src/hooks/__tests__/pipeline.test.ts +79 -0
- package/src/hooks/__tests__/registry.test.ts +67 -0
- package/src/hooks/bootstrap.ts +0 -2
- package/src/hooks/config.ts +29 -0
- package/src/hooks/index.ts +2 -1
- package/src/hooks/registry.ts +60 -1
- package/src/hooks/types.ts +21 -0
- package/src/index.ts +13 -22
- package/src/knowledge/import.ts +3 -3
- package/src/knowledge/ingest.ts +4 -4
- package/src/knowledge/lifecycle-hooks.ts +3 -3
- package/src/knowledge/types.ts +5 -8
- package/src/monitoring/constraint-doctor.ts +7 -106
- package/src/presets/__tests__/standard.test.ts +0 -2
- package/src/presets/standard.ts +1 -2
- package/src/spec/__tests__/annotation-checker.test.ts +9 -3
- package/src/spec/annotation-checker.ts +7 -29
- package/src/tools/CONTEXT.md +7 -14
- package/src/tools/index.ts +0 -4
- package/src/types/constraint.ts +1 -14
- package/src/types/project-config.ts +0 -3
- package/src/utils/file-walk.ts +2 -2
- package/dist/architecture/constraint-engine.d.ts +0 -67
- package/dist/architecture/constraint-engine.d.ts.map +0 -1
- package/dist/architecture/constraint-engine.js +0 -295
- package/dist/architecture/constraint-engine.js.map +0 -1
- package/dist/architecture/cross-project-checker.d.ts +0 -54
- package/dist/architecture/cross-project-checker.d.ts.map +0 -1
- package/dist/architecture/cross-project-checker.js +0 -467
- package/dist/architecture/cross-project-checker.js.map +0 -1
- package/dist/cli/commands/index.d.ts +0 -30
- package/dist/cli/commands/index.d.ts.map +0 -1
- package/dist/cli/commands/index.js +0 -91
- package/dist/cli/commands/index.js.map +0 -1
- package/dist/context/file-budget.d.ts +0 -40
- package/dist/context/file-budget.d.ts.map +0 -1
- package/dist/context/file-budget.js +0 -147
- package/dist/context/file-budget.js.map +0 -1
- package/dist/context/progressive-loader.d.ts +0 -109
- package/dist/context/progressive-loader.d.ts.map +0 -1
- package/dist/context/progressive-loader.js +0 -166
- package/dist/context/progressive-loader.js.map +0 -1
- package/dist/context/token-pipeline.d.ts +0 -55
- package/dist/context/token-pipeline.d.ts.map +0 -1
- package/dist/context/token-pipeline.js +0 -220
- package/dist/context/token-pipeline.js.map +0 -1
- package/dist/context/tool-output-budget.d.ts +0 -59
- package/dist/context/tool-output-budget.d.ts.map +0 -1
- package/dist/context/tool-output-budget.js +0 -177
- package/dist/context/tool-output-budget.js.map +0 -1
- package/dist/core/constraints/default-executors.d.ts +0 -10
- package/dist/core/constraints/default-executors.d.ts.map +0 -1
- package/dist/core/constraints/default-executors.js +0 -120
- package/dist/core/constraints/default-executors.js.map +0 -1
- package/dist/llm/adapter.d.ts +0 -36
- package/dist/llm/adapter.d.ts.map +0 -1
- package/dist/llm/adapter.js +0 -118
- package/dist/llm/adapter.js.map +0 -1
- package/dist/llm/index.d.ts +0 -3
- package/dist/llm/index.d.ts.map +0 -1
- package/dist/llm/index.js +0 -19
- package/dist/llm/index.js.map +0 -1
- package/dist/llm/types.d.ts +0 -62
- package/dist/llm/types.d.ts.map +0 -1
- package/dist/llm/types.js +0 -8
- package/dist/llm/types.js.map +0 -1
- package/dist/tools/core/index.d.ts +0 -13
- package/dist/tools/core/index.d.ts.map +0 -1
- package/dist/tools/core/index.js +0 -198
- package/dist/tools/core/index.js.map +0 -1
- package/dist/tools/loader.d.ts +0 -21
- package/dist/tools/loader.d.ts.map +0 -1
- package/dist/tools/loader.js +0 -199
- package/dist/tools/loader.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -59
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -115
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/types.d.ts +0 -38
- package/dist/tools/types.d.ts.map +0 -1
- package/dist/tools/types.js +0 -6
- package/dist/tools/types.js.map +0 -1
- package/src/__tests__/architecture-engine.test.ts +0 -619
- package/src/__tests__/context/progressive-loader.test.ts +0 -131
- package/src/__tests__/cross-project-checker.test.ts +0 -318
- package/src/architecture/CONTEXT.md +0 -22
- package/src/architecture/constraint-engine.ts +0 -351
- package/src/architecture/cross-project-checker.ts +0 -545
- package/src/cli/commands/index.ts +0 -30
- package/src/context/__tests__/file-budget.test.ts +0 -142
- package/src/context/__tests__/progressive-loader.test.ts +0 -264
- package/src/context/__tests__/token-pipeline.test.ts +0 -178
- package/src/context/__tests__/tool-output-budget.test.ts +0 -95
- package/src/context/file-budget.ts +0 -142
- package/src/context/progressive-loader.ts +0 -260
- package/src/context/token-pipeline.ts +0 -265
- package/src/context/tool-output-budget.ts +0 -178
- package/src/core/constraints/__tests__/default-executors.test.ts +0 -57
- package/src/core/constraints/default-executors.ts +0 -92
- package/src/llm/CONTEXT.md +0 -21
- package/src/llm/__tests__/adapter.test.ts +0 -172
- package/src/llm/adapter.ts +0 -149
- package/src/llm/index.ts +0 -2
- package/src/llm/types.ts +0 -81
- package/src/tools/__tests__/loader.test.ts +0 -252
- package/src/tools/__tests__/registry.test.ts +0 -157
- package/src/tools/core/__tests__/core.test.ts +0 -122
- package/src/tools/core/index.ts +0 -201
- package/src/tools/loader.ts +0 -216
- package/src/tools/registry.ts +0 -128
- package/src/tools/types.ts +0 -46
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trigger 参数化约束分组渲染 API 测试(H6/G6,收编 studio prompt-injection.ts)
|
|
3
|
+
*
|
|
4
|
+
* 语义:
|
|
5
|
+
* - 入参 = 触发条件(单 trigger 或数组),无 role 概念(role→trigger 路由留 studio)
|
|
6
|
+
* - 数据源 = getEffectiveConstraints(内置 → preset → config.yml 禁用 → custom 追加 → scenes 过滤)
|
|
7
|
+
* - 输出按层级分组:铁律 → 指导原则 → 行为提示;无 promptInjection 的约束不渲染
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { renderConstraintsByTrigger } from '../agent-prompt-renderer';
|
|
14
|
+
import { renderConstraintsByTrigger as PublicRenderConstraintsByTrigger } from '../../../index';
|
|
15
|
+
|
|
16
|
+
describe('公开导出(根 barrel)', () => {
|
|
17
|
+
test('renderConstraintsByTrigger 经 src/index.ts 公开导出', () => {
|
|
18
|
+
expect(PublicRenderConstraintsByTrigger).toBe(renderConstraintsByTrigger);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** 创建临时项目目录,可按相对路径写入 .harness/config.yml 等夹具文件 */
|
|
23
|
+
function makeProject(files: Record<string, string>): string {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'h6-render-'));
|
|
25
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
26
|
+
const target = path.join(dir, rel);
|
|
27
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
28
|
+
fs.writeFileSync(target, content, 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('renderConstraintsByTrigger', () => {
|
|
34
|
+
test('内置约束按层级分组渲染:铁律 → 指导原则 → 行为提示', () => {
|
|
35
|
+
const projectRoot = makeProject({});
|
|
36
|
+
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
37
|
+
|
|
38
|
+
expect(out).toContain('## 行为约束(前置声明)');
|
|
39
|
+
expect(out).toContain('### 铁律(绝对禁止,无例外)');
|
|
40
|
+
expect(out).toContain('- **no_completion_without_verification**:');
|
|
41
|
+
expect(out).toContain('### 指导原则(优先建议)');
|
|
42
|
+
expect(out).toContain('- **no_hardcoded_credentials**:');
|
|
43
|
+
expect(out).toContain('### 行为提示');
|
|
44
|
+
expect(out).toContain('- **no_code_without_test**:');
|
|
45
|
+
|
|
46
|
+
// 分组顺序:铁律 < 指导原则 < 行为提示
|
|
47
|
+
const ironIdx = out.indexOf('### 铁律(绝对禁止,无例外)');
|
|
48
|
+
const guideIdx = out.indexOf('### 指导原则(优先建议)');
|
|
49
|
+
const promptIdx = out.indexOf('### 行为提示');
|
|
50
|
+
expect(ironIdx).toBeGreaterThanOrEqual(0);
|
|
51
|
+
expect(guideIdx).toBeGreaterThan(ironIdx);
|
|
52
|
+
expect(promptIdx).toBeGreaterThan(guideIdx);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('无 promptInjection 的约束不渲染(即使 trigger 匹配)', () => {
|
|
56
|
+
const projectRoot = makeProject({});
|
|
57
|
+
// docs_freshness / capability_sync / context_doc_sync 无 promptInjection
|
|
58
|
+
const out = renderConstraintsByTrigger('module_modification', { projectRoot });
|
|
59
|
+
|
|
60
|
+
expect(out).not.toContain('- **docs_freshness**:');
|
|
61
|
+
expect(out).not.toContain('- **capability_sync**:');
|
|
62
|
+
expect(out).not.toContain('- **context_doc_sync**:');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('支持 trigger 数组入参,仅匹配交集约束', () => {
|
|
66
|
+
const projectRoot = makeProject({});
|
|
67
|
+
const out = renderConstraintsByTrigger(['test_creation'], { projectRoot });
|
|
68
|
+
|
|
69
|
+
expect(out).toContain('- **no_test_simplification**:');
|
|
70
|
+
// no_completion_without_verification 的 trigger 为 code_implementation,不匹配
|
|
71
|
+
expect(out).not.toContain('- **no_completion_without_verification**:');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('无匹配约束返回空字符串', () => {
|
|
75
|
+
const projectRoot = makeProject({});
|
|
76
|
+
expect(renderConstraintsByTrigger('nonexistent_trigger_xyz', { projectRoot })).toBe('');
|
|
77
|
+
expect(renderConstraintsByTrigger([], { projectRoot })).toBe('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('config.yml 禁用的约束不进入渲染(生效集数据源)', () => {
|
|
81
|
+
const projectRoot = makeProject({
|
|
82
|
+
'.harness/config.yml': [
|
|
83
|
+
'constraints:',
|
|
84
|
+
' no_hardcoded_credentials:',
|
|
85
|
+
' enabled: false',
|
|
86
|
+
'',
|
|
87
|
+
].join('\n'),
|
|
88
|
+
});
|
|
89
|
+
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
90
|
+
|
|
91
|
+
expect(out).not.toContain('- **no_hardcoded_credentials**:');
|
|
92
|
+
// 同 trigger 的其它约束仍渲染
|
|
93
|
+
expect(out).toContain('- **no_completion_without_verification**:');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('custom 约束追加进生效集并参与分组渲染', () => {
|
|
97
|
+
const projectRoot = makeProject({
|
|
98
|
+
'.harness/config.yml': [
|
|
99
|
+
'custom_constraints:',
|
|
100
|
+
' my_custom_rule:',
|
|
101
|
+
' level: prompt',
|
|
102
|
+
' trigger: code_implementation',
|
|
103
|
+
' rule: CUSTOM RULE',
|
|
104
|
+
' message: 自定义约束消息',
|
|
105
|
+
' promptInjection: 自定义行为提示文本',
|
|
106
|
+
'',
|
|
107
|
+
].join('\n'),
|
|
108
|
+
});
|
|
109
|
+
const out = renderConstraintsByTrigger('code_implementation', { projectRoot });
|
|
110
|
+
|
|
111
|
+
expect(out).toContain('- **my_custom_rule**: 自定义行为提示文本');
|
|
112
|
+
const promptIdx = out.indexOf('### 行为提示');
|
|
113
|
+
const customIdx = out.indexOf('- **my_custom_rule**:');
|
|
114
|
+
expect(customIdx).toBeGreaterThan(promptIdx);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('scenes 过滤:带 appliesTo 的场景专属 prompt 默认不渲染,声明场景后渲染', () => {
|
|
118
|
+
// no_skill_without_test:appliesTo ['agent-skill'],trigger module_creation
|
|
119
|
+
const plain = makeProject({});
|
|
120
|
+
expect(renderConstraintsByTrigger('module_creation', { projectRoot: plain }))
|
|
121
|
+
.not.toContain('- **no_skill_without_test**:');
|
|
122
|
+
|
|
123
|
+
const withScene = makeProject({
|
|
124
|
+
'.harness/config.yml': 'scenes:\n - agent-skill\n',
|
|
125
|
+
});
|
|
126
|
+
expect(renderConstraintsByTrigger('module_creation', { projectRoot: withScene }))
|
|
127
|
+
.toContain('- **no_skill_without_test**:');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CheckCache 测试(H6/G5:TTL 缓存既有行为 + 计数采样扩展)
|
|
3
|
+
*
|
|
4
|
+
* 计数采样语义(与 studio runtime/cache.ts 收编对齐):
|
|
5
|
+
* - 每个 (namespace, key) 独立计数
|
|
6
|
+
* - 第 1 次调用必执行(采样轮),此后每 N 次执行 1 次
|
|
7
|
+
* - 非采样轮:缓存命中(未过期)返回缓存值;未命中/已过期返回 defaultValueOnMiss,不执行 fn
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CheckCache } from '../check-cache';
|
|
11
|
+
import { CheckCache as PublicCheckCache } from '../../../index';
|
|
12
|
+
|
|
13
|
+
describe('公开导出(根 barrel)', () => {
|
|
14
|
+
test('CheckCache 经 src/index.ts 公开导出', () => {
|
|
15
|
+
expect(PublicCheckCache).toBe(CheckCache);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('CheckCache', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.useFakeTimers();
|
|
22
|
+
jest.setSystemTime(new Date('2026-01-01T00:00:00.000Z'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
jest.useRealTimers();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('TTL 缓存(既有行为,不传 sampling 参数)', () => {
|
|
30
|
+
test('缓存命中不重复执行 fn,过期后重新执行', async () => {
|
|
31
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
32
|
+
const fn = jest.fn(async () => 'v1');
|
|
33
|
+
|
|
34
|
+
expect(await cache.get('ns', 'k', fn)).toBe('v1');
|
|
35
|
+
expect(await cache.get('ns', 'k', fn)).toBe('v1');
|
|
36
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
37
|
+
|
|
38
|
+
jest.advanceTimersByTime(1001);
|
|
39
|
+
expect(await cache.get('ns', 'k', fn)).toBe('v1');
|
|
40
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('getSync 命中/过期行为一致', () => {
|
|
44
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
45
|
+
const fn = jest.fn(() => 'sync');
|
|
46
|
+
|
|
47
|
+
expect(cache.getSync('ns', 'k', fn)).toBe('sync');
|
|
48
|
+
expect(cache.getSync('ns', 'k', fn)).toBe('sync');
|
|
49
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
50
|
+
|
|
51
|
+
jest.advanceTimersByTime(1001);
|
|
52
|
+
expect(cache.getSync('ns', 'k', fn)).toBe('sync');
|
|
53
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('缺省构造使用默认 TTL 5000ms', async () => {
|
|
57
|
+
const cache = new CheckCache();
|
|
58
|
+
const fn = jest.fn(async () => 'v');
|
|
59
|
+
|
|
60
|
+
await cache.get('ns', 'k', fn);
|
|
61
|
+
jest.advanceTimersByTime(4999);
|
|
62
|
+
await cache.get('ns', 'k', fn);
|
|
63
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
64
|
+
|
|
65
|
+
jest.advanceTimersByTime(2); // 累计 5001ms,已过期
|
|
66
|
+
await cache.get('ns', 'k', fn);
|
|
67
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('命名空间隔离:不同 namespace 相同 key 互不影响', async () => {
|
|
71
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
72
|
+
const f1 = jest.fn(async () => 'a');
|
|
73
|
+
const f2 = jest.fn(async () => 'b');
|
|
74
|
+
|
|
75
|
+
expect(await cache.get('ns1', 'k', f1)).toBe('a');
|
|
76
|
+
expect(await cache.get('ns2', 'k', f2)).toBe('b');
|
|
77
|
+
expect(f1).toHaveBeenCalledTimes(1);
|
|
78
|
+
expect(f2).toHaveBeenCalledTimes(1);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('invalidate(namespace) 仅清除指定命名空间', async () => {
|
|
82
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
83
|
+
const f1 = jest.fn(async () => 'a');
|
|
84
|
+
const f2 = jest.fn(async () => 'b');
|
|
85
|
+
|
|
86
|
+
await cache.get('ns1', 'k', f1);
|
|
87
|
+
await cache.get('ns2', 'k', f2);
|
|
88
|
+
cache.invalidate('ns1');
|
|
89
|
+
|
|
90
|
+
expect(await cache.get('ns1', 'k', f1)).toBe('a');
|
|
91
|
+
expect(await cache.get('ns2', 'k', f2)).toBe('b');
|
|
92
|
+
expect(f1).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(f2).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('invalidate() 清除全部缓存', async () => {
|
|
97
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
98
|
+
const fn = jest.fn(async () => 'v');
|
|
99
|
+
|
|
100
|
+
await cache.get('ns', 'k1', fn);
|
|
101
|
+
await cache.get('ns', 'k2', fn);
|
|
102
|
+
cache.invalidate();
|
|
103
|
+
|
|
104
|
+
await cache.get('ns', 'k1', fn);
|
|
105
|
+
await cache.get('ns', 'k2', fn);
|
|
106
|
+
expect(fn).toHaveBeenCalledTimes(4);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('计数采样(声明式配置)', () => {
|
|
111
|
+
test('第 1 次调用必执行,此后每 N 次执行 1 次,其余复用缓存结果', async () => {
|
|
112
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
113
|
+
let runs = 0;
|
|
114
|
+
const fn = jest.fn(async () => `run-${++runs}`);
|
|
115
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'skip' };
|
|
116
|
+
|
|
117
|
+
// 第 1 次:采样轮 → 执行
|
|
118
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('run-1');
|
|
119
|
+
// 第 2、3 次:非采样轮 → 复用缓存
|
|
120
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('run-1');
|
|
121
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('run-1');
|
|
122
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
123
|
+
// 第 4 次:采样轮 → 执行
|
|
124
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('run-2');
|
|
125
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('非采样轮缓存已过期:返回 defaultValueOnMiss,不执行 fn', async () => {
|
|
129
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
130
|
+
const fn = jest.fn(async () => 'fresh');
|
|
131
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'skip' };
|
|
132
|
+
|
|
133
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('fresh'); // 采样轮,TTL=1s 缓存
|
|
134
|
+
jest.advanceTimersByTime(1001);
|
|
135
|
+
|
|
136
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('skip'); // 过期 + 非采样轮
|
|
137
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('skip');
|
|
138
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
139
|
+
|
|
140
|
+
// 第 4 次:采样轮 → 重新执行
|
|
141
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('fresh');
|
|
142
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('采样计数器按 key 独立', async () => {
|
|
146
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
147
|
+
const f1 = jest.fn(async () => 'a');
|
|
148
|
+
const f2 = jest.fn(async () => 'b');
|
|
149
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'x' };
|
|
150
|
+
|
|
151
|
+
await cache.get('ns', 'k1', f1, sampling);
|
|
152
|
+
await cache.get('ns', 'k2', f2, sampling);
|
|
153
|
+
expect(f1).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(f2).toHaveBeenCalledTimes(1);
|
|
155
|
+
|
|
156
|
+
await cache.get('ns', 'k1', f1, sampling); // k1 非采样轮
|
|
157
|
+
expect(f1).toHaveBeenCalledTimes(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('invalidate 重置计数器:失效后下一次调用恢复为采样轮', async () => {
|
|
161
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
162
|
+
const fn = jest.fn(async () => 'v');
|
|
163
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'd' };
|
|
164
|
+
|
|
165
|
+
await cache.get('ns', 'k', fn, sampling); // count=1 采样
|
|
166
|
+
await cache.get('ns', 'k', fn, sampling); // count=2 跳过
|
|
167
|
+
cache.invalidate('ns');
|
|
168
|
+
await cache.get('ns', 'k', fn, sampling); // 重置后 count=1 采样
|
|
169
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('invalidate() 全量同样重置计数器', async () => {
|
|
173
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
174
|
+
const fn = jest.fn(async () => 'v');
|
|
175
|
+
const sampling = { sampleRate: 2, defaultValueOnMiss: 'd' };
|
|
176
|
+
|
|
177
|
+
await cache.get('ns', 'k', fn, sampling); // count=1 采样
|
|
178
|
+
cache.invalidate();
|
|
179
|
+
await cache.get('ns', 'k', fn, sampling); // 重置后 count=1 采样
|
|
180
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('sampleRate <= 1 视为不采样:回退为普通 TTL 缓存语义', async () => {
|
|
184
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
185
|
+
const fn = jest.fn(async () => 'v');
|
|
186
|
+
|
|
187
|
+
await cache.get('ns', 'k', fn, { sampleRate: 1, defaultValueOnMiss: 'd' });
|
|
188
|
+
await cache.get('ns', 'k', fn, { sampleRate: 0, defaultValueOnMiss: 'd' });
|
|
189
|
+
expect(fn).toHaveBeenCalledTimes(1); // TTL 命中,无采样短路
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('getSync 同样支持计数采样', () => {
|
|
193
|
+
const cache = new CheckCache({ ttlMs: 60_000 });
|
|
194
|
+
const fn = jest.fn(() => 'sync');
|
|
195
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'skip' };
|
|
196
|
+
|
|
197
|
+
expect(cache.getSync('ns', 'k', fn, sampling)).toBe('sync'); // 采样轮
|
|
198
|
+
expect(cache.getSync('ns', 'k', fn, sampling)).toBe('sync'); // 复用缓存
|
|
199
|
+
expect(cache.getSync('ns', 'k', fn, sampling)).toBe('sync'); // 复用缓存
|
|
200
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('getSync 非采样轮缓存过期返回默认值', () => {
|
|
204
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
205
|
+
const fn = jest.fn(() => 'sync');
|
|
206
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'skip' };
|
|
207
|
+
|
|
208
|
+
expect(cache.getSync('ns', 'k', fn, sampling)).toBe('sync'); // 采样轮 @t0
|
|
209
|
+
jest.advanceTimersByTime(1001);
|
|
210
|
+
expect(cache.getSync('ns', 'k', fn, sampling)).toBe('skip'); // 过期 + 非采样轮
|
|
211
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('采样轮执行结果以正常 TTL 写入缓存,非采样轮不刷新 TTL', async () => {
|
|
215
|
+
const cache = new CheckCache({ ttlMs: 1000 });
|
|
216
|
+
const fn = jest.fn(async () => 'v');
|
|
217
|
+
const sampling = { sampleRate: 3, defaultValueOnMiss: 'd' };
|
|
218
|
+
|
|
219
|
+
await cache.get('ns', 'k', fn, sampling); // 采样轮 @t0
|
|
220
|
+
jest.advanceTimersByTime(500);
|
|
221
|
+
await cache.get('ns', 'k', fn, sampling); // 非采样轮 @t0+500(不应刷新过期时间)
|
|
222
|
+
jest.advanceTimersByTime(600); // t0+1100:若 TTL 被刷新则仍命中
|
|
223
|
+
expect(await cache.get('ns', 'k', fn, sampling)).toBe('d'); // 已过期 → 默认值
|
|
224
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -64,7 +64,6 @@ describe('ConstraintInterceptor', () => {
|
|
|
64
64
|
mockChecker.getConstraints.mockReturnValue({
|
|
65
65
|
ironLaws: { test_iron_law: mockIronLaw },
|
|
66
66
|
guidelines: { test_guideline: mockGuideline },
|
|
67
|
-
tips: {},
|
|
68
67
|
prompts: {},
|
|
69
68
|
});
|
|
70
69
|
});
|
|
@@ -202,7 +201,6 @@ describe('ConstraintInterceptor', () => {
|
|
|
202
201
|
mockChecker.getConstraints.mockReturnValue({
|
|
203
202
|
ironLaws: {},
|
|
204
203
|
guidelines: {},
|
|
205
|
-
tips: {},
|
|
206
204
|
prompts: {},
|
|
207
205
|
});
|
|
208
206
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent prompt 约束段渲染(H6/G6:trigger 参数化分组渲染 API)
|
|
3
|
+
*
|
|
4
|
+
* 收编 studio prompt-injection.ts 的渲染层:role→trigger 路由留在 studio,
|
|
5
|
+
* 本模块只做 trigger 参数化 + 生效集过滤 + 层级分组渲染。
|
|
6
|
+
*
|
|
7
|
+
* 数据源 = getEffectiveConstraints(内置 → preset → config.yml 禁用 →
|
|
8
|
+
* custom 追加 → scenes 过滤),与 init 注入、harness check 同一来源。
|
|
9
|
+
*
|
|
10
|
+
* 输出按层级分组:铁律 → 指导原则 → 行为提示;无 promptInjection 的约束
|
|
11
|
+
* (如 docs_freshness)不渲染。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Constraint, ConstraintLevel, ConstraintTrigger } from '../../types/constraint';
|
|
15
|
+
import { normalizeTriggers } from '../../utils/exec';
|
|
16
|
+
import { getEffectiveConstraints } from '../effective-constraints';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* renderConstraintsByTrigger 选项
|
|
20
|
+
*/
|
|
21
|
+
export interface RenderConstraintsByTriggerOptions {
|
|
22
|
+
/** 项目根路径(决定 config.yml 生效集),缺省 process.cwd() */
|
|
23
|
+
projectRoot?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 层级分组定义(渲染顺序即声明顺序) */
|
|
27
|
+
const LEVEL_GROUPS: ReadonlyArray<{ level: ConstraintLevel; heading: string }> = [
|
|
28
|
+
{ level: 'iron_law', heading: '### 铁律(绝对禁止,无例外)' },
|
|
29
|
+
{ level: 'guideline', heading: '### 指导原则(优先建议)' },
|
|
30
|
+
{ level: 'prompt', heading: '### 行为提示' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 按触发条件渲染约束分组文本(注入 Agent system prompt 用)
|
|
35
|
+
*
|
|
36
|
+
* 过滤:生效集内 trigger 与入参存在交集的约束;无 promptInjection 的不渲染。
|
|
37
|
+
* 分组:按层级 iron_law → guideline → prompt 顺序渲染为标题 + 条目列表;
|
|
38
|
+
* 无适用约束时返回空字符串。
|
|
39
|
+
*
|
|
40
|
+
* @param triggers 触发条件(单个或多个;空数组返回空字符串)
|
|
41
|
+
* @param options.projectRoot 项目根路径(决定 config.yml 生效集)
|
|
42
|
+
*/
|
|
43
|
+
export function renderConstraintsByTrigger(
|
|
44
|
+
triggers: ConstraintTrigger | ConstraintTrigger[],
|
|
45
|
+
options?: RenderConstraintsByTriggerOptions,
|
|
46
|
+
): string {
|
|
47
|
+
const requested = Array.isArray(triggers) ? triggers : [triggers];
|
|
48
|
+
if (requested.length === 0) return '';
|
|
49
|
+
|
|
50
|
+
const constraints = getEffectiveConstraints(options?.projectRoot);
|
|
51
|
+
const applicable = constraints.filter(
|
|
52
|
+
c => c.promptInjection && matchesTrigger(c, requested)
|
|
53
|
+
);
|
|
54
|
+
if (applicable.length === 0) return '';
|
|
55
|
+
|
|
56
|
+
const lines: string[] = ['\n## 行为约束(前置声明)\n'];
|
|
57
|
+
for (const group of LEVEL_GROUPS) {
|
|
58
|
+
const items = applicable.filter(c => c.level === group.level);
|
|
59
|
+
if (items.length === 0) continue;
|
|
60
|
+
lines.push(`${group.heading}\n`);
|
|
61
|
+
for (const c of items) {
|
|
62
|
+
lines.push(`- **${c.id}**: ${c.promptInjection}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function matchesTrigger(constraint: Constraint, requested: ConstraintTrigger[]): boolean {
|
|
70
|
+
return normalizeTriggers<ConstraintTrigger>(constraint.trigger).some(t => requested.includes(t));
|
|
71
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CAPABILITIES.md
|
|
2
|
+
* CAPABILITIES.md 解析与能力清单计数(工单 19-B / H2 O10-R3)
|
|
3
3
|
*
|
|
4
4
|
* 此前 checker.parseCapabilitiesFiles 与 sync-docs.parseCapabilitiesFiles
|
|
5
|
-
* 各持一份正则,收敛到此处。
|
|
5
|
+
* 各持一份正则,收敛到此处。H2 起能力清单计数(checkCapabilityCounts /
|
|
6
|
+
* updateCapabilityCounts)也从 sync-docs/capabilities-syncer 收敛到此,
|
|
7
|
+
* 使本模块成为 CAPABILITIES.md 解析 + 计数唯一入口。
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import * as fs from 'fs';
|
|
11
|
+
import { IRON_LAWS, GUIDELINES } from './definitions';
|
|
12
|
+
import { FreshnessRunner } from './doc-freshness/runner';
|
|
13
|
+
import type { DocFreshnessCheck } from '../../types/project-config';
|
|
9
14
|
|
|
10
15
|
/** 匹配表格单元格中的源码文件条目(如 `src/foo.ts`、`foo.tsx`) */
|
|
11
16
|
const FILE_CELL_REGEX = /\|\s*([^|]+?\.(?:ts|tsx|js|jsx))\s*\|/g;
|
|
@@ -96,3 +101,91 @@ export function aggregateToSourceSubdir(root: string, file: string): string {
|
|
|
96
101
|
if (idx === -1) return prefix;
|
|
97
102
|
return prefix + rest.slice(0, idx) + '/';
|
|
98
103
|
}
|
|
104
|
+
|
|
105
|
+
// ── 能力清单计数 ────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 构建 CAPABILITIES.md 能力清单格式的检查配置
|
|
109
|
+
*/
|
|
110
|
+
function buildCapabilityChecks(): DocFreshnessCheck[] {
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
type: 'doc_regex_count',
|
|
114
|
+
doc: 'CAPABILITIES.md',
|
|
115
|
+
label: 'CLI Commands',
|
|
116
|
+
pattern: 'CLI Commands\\s*\\((\\d+)\\)',
|
|
117
|
+
// index.ts(已删 barrel)与 definitions.ts(命令注册表纯数据)不是命令实现,不计入
|
|
118
|
+
actual: { kind: 'dir_count', path: 'src/cli/commands', extension: '.ts', exclude: ['index.ts', 'definitions.ts'] },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: 'doc_regex_count',
|
|
122
|
+
doc: 'CAPABILITIES.md',
|
|
123
|
+
label: 'Quality Gates',
|
|
124
|
+
pattern: 'Quality Gates?\\s*\\((\\d+)\\)',
|
|
125
|
+
actual: { kind: 'dir_count', path: 'src/gates', extension: '.ts', exclude: ['index.ts', 'types.ts'] },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: 'doc_regex_count',
|
|
129
|
+
doc: 'CAPABILITIES.md',
|
|
130
|
+
label: 'Iron Laws',
|
|
131
|
+
pattern: 'Iron Laws?\\s*\\((\\d+)\\)',
|
|
132
|
+
actual: { kind: 'const_count', value: Object.keys(IRON_LAWS).length },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: 'doc_regex_count',
|
|
136
|
+
doc: 'CAPABILITIES.md',
|
|
137
|
+
label: 'Guidelines',
|
|
138
|
+
pattern: 'Guidelines?\\s*\\((\\d+)\\)',
|
|
139
|
+
actual: { kind: 'const_count', value: Object.keys(GUIDELINES).length },
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 使用 FreshnessRunner 检查能力清单计数是否与代码一致(--check 模式)
|
|
146
|
+
*/
|
|
147
|
+
export function checkCapabilityCounts(
|
|
148
|
+
projectPath: string
|
|
149
|
+
): { match: boolean; mismatches: string[] } {
|
|
150
|
+
const runner = new FreshnessRunner();
|
|
151
|
+
const checks = buildCapabilityChecks();
|
|
152
|
+
const results = runner.runAll({ checks }, projectPath);
|
|
153
|
+
|
|
154
|
+
const mismatches: string[] = [];
|
|
155
|
+
for (const r of results) {
|
|
156
|
+
if (!r.pass && r.message) {
|
|
157
|
+
mismatches.push(r.message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { match: mismatches.length === 0, mismatches };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 更新 CAPABILITIES.md 中的能力清单计数(write 模式)
|
|
166
|
+
*
|
|
167
|
+
* 使用 FreshnessRunner 获取实际计数,然后 regex 替换文档计数行。
|
|
168
|
+
*/
|
|
169
|
+
export function updateCapabilityCounts(content: string, projectPath: string): string {
|
|
170
|
+
const runner = new FreshnessRunner();
|
|
171
|
+
|
|
172
|
+
const cliCount = runner.countFromDir('src/cli/commands', '.ts', ['index.ts', 'definitions.ts'], projectPath);
|
|
173
|
+
const gateCount = runner.countFromDir('src/gates', '.ts', ['index.ts', 'types.ts'], projectPath);
|
|
174
|
+
const ironCount = Object.keys(IRON_LAWS).length;
|
|
175
|
+
const guideCount = Object.keys(GUIDELINES).length;
|
|
176
|
+
|
|
177
|
+
const replacements: [RegExp, string][] = [
|
|
178
|
+
[/CLI Commands\s*\(\d+\)/, `CLI Commands (${cliCount})`],
|
|
179
|
+
[/Quality Gates?\s*\(\d+\)/, `Quality Gates (${gateCount})`],
|
|
180
|
+
[/Iron Laws?\s*\(\d+\)/, `Iron Laws (${ironCount})`],
|
|
181
|
+
[/Guidelines?\s*\(\d+\)/, `Guidelines (${guideCount})`],
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const [regex, replacement] of replacements) {
|
|
185
|
+
if (regex.test(content)) {
|
|
186
|
+
content = content.replace(regex, replacement);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return content;
|
|
191
|
+
}
|