phasegate 0.283.0 → 0.335.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 +16 -0
- package/docs/ADR/017-warning-severity-aggregation.md +17 -0
- package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
- package/docs/ADR/039-hook-observable-state-as-authorization-unit.md +74 -0
- package/docs/ADR/040-quick-mode-config-via-preset-resolution.md +66 -0
- package/docs/guide/cli-reference.md +89 -7
- package/docs/guide/configuration.md +43 -2
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +63 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/dto/handle-pre-tool-use-dto.ts +5 -1
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -20
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +215 -5
- package/scripts/harness/agent-integration/domain/value-objects/protected-file-list.ts +5 -0
- package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
- package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +18 -3
- package/scripts/harness/agent-integration/infrastructure/adapters/quick-mode-full-mode-requirement-adapter.ts +15 -1
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +57 -9
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-input.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/scaffold-inception-output.ts +13 -0
- package/scripts/harness/ci-governance/application/usecases/list-templates-usecase.ts +30 -0
- package/scripts/harness/ci-governance/application/usecases/scaffold-inception-usecase.ts +63 -0
- package/scripts/harness/ci-governance/application/usecases/show-template-usecase.ts +39 -0
- package/scripts/harness/ci-governance/composition-root.ts +57 -5
- package/scripts/harness/ci-governance/domain/ports/inception-doc-writer-port.ts +19 -0
- package/scripts/harness/ci-governance/domain/ports/inception-template-repository-port.ts +16 -0
- package/scripts/harness/ci-governance/domain/ports/template-catalog-port.ts +23 -0
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/domain/value-objects/inception-doc-kind.ts +109 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-catalog-entry.ts +53 -0
- package/scripts/harness/ci-governance/domain/value-objects/template-name.ts +50 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-doc-writer-adapter.ts +42 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-inception-template-repository-adapter.ts +32 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-template-catalog-adapter.ts +60 -0
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/ci-governance/presentation/handlers/scaffold-inception-handler.ts +101 -0
- package/scripts/harness/ci-governance/presentation/handlers/templates-handler.ts +103 -0
- package/scripts/harness/config-foundation/domain/harness-config.ts +12 -0
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +3 -3
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +44 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +42 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +12 -3
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +3 -1
- package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
- package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
- package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
- package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
- package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
- package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
- package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
- package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
- package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
- package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
- package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
- package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
- package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
- package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
- package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
- package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
- package/scripts/harness/installation/application/usecases/run-install.ts +44 -11
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +44 -11
- package/scripts/harness/installation/composition-root.ts +13 -3
- package/scripts/harness/installation/domain/check-id.ts +2 -0
- package/scripts/harness/installation/domain/config-status.ts +17 -0
- package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
- package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
- package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
- package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
- package/scripts/harness/integrations/pre-commit.ts +17 -3
- package/scripts/harness/main.ts +159 -21
- package/scripts/harness/phase-dependency-model/application/services/evidence-bundle-assembler.ts +2 -1
- package/scripts/harness/phase-dependency-model/domain/ports/plan-document-reader-port.ts +12 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +64 -33
- package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
- package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
- package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
- package/scripts/harness/quick-mode/composition-root.ts +25 -15
- package/scripts/harness/quick-mode/domain/errors/quick-mode-config-error.ts +19 -0
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +116 -18
- package/scripts/harness/quick-mode/domain/value-objects/category-override-rules.ts +147 -0
- package/scripts/harness/quick-mode/domain/value-objects/change-category.ts +33 -0
- package/scripts/harness/quick-mode/domain/value-objects/quick-mode-config.ts +31 -10
- package/scripts/harness/quick-mode/index.ts +7 -1
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- package/scripts/harness/quick-mode/infrastructure/adapters/harness-config-quick-mode-config-adapter.ts +83 -32
- package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
- package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
- package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
- package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +8 -0
- package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
- package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
- package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
- package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
- package/skills/product-architect/SKILL.md +19 -0
- package/skills/quick-implementor/SKILL.md +19 -0
- package/skills/story-mapper/SKILL.md +11 -0
- package/skills/story-writer/SKILL.md +11 -0
- package/skills/unit-designer/SKILL.md +11 -0
- package/templates/product_overview.template.md +85 -0
- package/templates/product_overview_plan.template.md +55 -0
- package/templates/story_mapping_plan.template.md +61 -0
- package/templates/story_writer_plan.template.md +61 -0
- package/templates/unit_design_plan.template.md +63 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer application
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-334
|
|
5
|
+
*
|
|
6
|
+
* ファイルの存在確認を行う Application 層 Port。
|
|
7
|
+
* ClassifyChangeCategoryUseCase が targetChanges を受け取らない経路
|
|
8
|
+
* (CLI: check-change-category --paths)で changeKind を CREATE/MODIFY と
|
|
9
|
+
* 推定するために使用する。hook 経路(beforeContent=null → CREATE)との
|
|
10
|
+
* 判定一致を保証する目的で導入された(WI-334)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface FileExistencePort {
|
|
14
|
+
exists(filePath: string): Promise<boolean>;
|
|
15
|
+
}
|
|
@@ -6,10 +6,15 @@
|
|
|
6
6
|
* paths から変更カテゴリを分類し fullModeRequired 判定と理由を返す UseCase
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import type {
|
|
9
|
+
import { QuickModeJudgmentEngine } from "../../domain/services/quick-mode-judgment-engine.js";
|
|
10
|
+
import type { ChangeKind } from "../../domain/types/change-kind.js";
|
|
11
|
+
import { ChangedFile } from "../../domain/value-objects/changed-file.js";
|
|
12
|
+
import type {
|
|
13
|
+
ChangeCategoryClassificationContract,
|
|
14
|
+
ChangeCategoryPerFile,
|
|
15
|
+
} from "../dto/change-category-classification-contract.js";
|
|
16
|
+
import type { FileExistencePort } from "../ports/file-existence-port.js";
|
|
17
|
+
import type { QuickModeConfigPort } from "../ports/quick-mode-config-port.js";
|
|
13
18
|
|
|
14
19
|
export interface ClassifyChangeCategoryUseCaseInput {
|
|
15
20
|
readonly paths: readonly string[];
|
|
@@ -23,20 +28,41 @@ export interface ClassifyChangeCategoryUseCaseInput {
|
|
|
23
28
|
export interface ClassifyChangeCategoryUseCaseDeps {
|
|
24
29
|
quickModeConfigPort: QuickModeConfigPort;
|
|
25
30
|
judgmentEngine?: QuickModeJudgmentEngine;
|
|
31
|
+
fileExistencePort?: FileExistencePort;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export class ClassifyChangeCategoryUseCase {
|
|
29
35
|
private readonly quickModeConfigPort: QuickModeConfigPort;
|
|
30
36
|
private readonly judgmentEngine: QuickModeJudgmentEngine;
|
|
37
|
+
private readonly fileExistencePort?: FileExistencePort;
|
|
31
38
|
|
|
32
39
|
constructor(deps: ClassifyChangeCategoryUseCaseDeps) {
|
|
33
40
|
this.quickModeConfigPort = deps.quickModeConfigPort;
|
|
34
41
|
this.judgmentEngine = deps.judgmentEngine ?? new QuickModeJudgmentEngine();
|
|
42
|
+
this.fileExistencePort = deps.fileExistencePort;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
/**
|
|
46
|
+
* targetChanges 引数自体が渡されない経路(CLI: check-change-category --paths)向けの
|
|
47
|
+
* changeKind 推定。hook 経路は beforeContent=null → CREATE と判定するため、同一パスで
|
|
48
|
+
* CLI=MODIFY(bugfix) / hook=CREATE(feature) に割れていた(WI-334)。
|
|
49
|
+
* ファイルが存在しない場合は CREATE、存在する場合は MODIFY と推定して hook 判定と一致させる。
|
|
50
|
+
* port 未注入・存在チェック失敗時は従来どおり MODIFY 既定(安全側)。
|
|
51
|
+
* なお hook は targetChanges を常に配列(空を含む)で渡すため推定対象外となり、
|
|
52
|
+
* Bash 抽出ターゲット等(配列にエントリが無いパス)の従来挙動は変えない。
|
|
53
|
+
*/
|
|
54
|
+
private async inferChangeKind(filePath: string): Promise<ChangeKind> {
|
|
55
|
+
if (this.fileExistencePort === undefined) {
|
|
56
|
+
return "MODIFY";
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return (await this.fileExistencePort.exists(filePath)) ? "MODIFY" : "CREATE";
|
|
60
|
+
} catch {
|
|
61
|
+
return "MODIFY";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async execute(input: ClassifyChangeCategoryUseCaseInput): Promise<Readonly<ChangeCategoryClassificationContract>> {
|
|
40
66
|
const config = await this.quickModeConfigPort.getConfig();
|
|
41
67
|
|
|
42
68
|
if (input.paths.length === 0) {
|
|
@@ -47,21 +73,33 @@ export class ClassifyChangeCategoryUseCase {
|
|
|
47
73
|
});
|
|
48
74
|
}
|
|
49
75
|
|
|
76
|
+
const inferenceEnabled = input.targetChanges === undefined;
|
|
50
77
|
const targetChanges = new Map((input.targetChanges ?? []).map((change) => [change.filePath, change]));
|
|
51
|
-
const changedFiles =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
const changedFiles = await Promise.all(
|
|
79
|
+
input.paths.map(async (p) => {
|
|
80
|
+
const targetChange = targetChanges.get(p);
|
|
81
|
+
if (targetChange === undefined && inferenceEnabled) {
|
|
82
|
+
// targetChanges 引数が渡されない経路(CLI)はファイル存在で CREATE/MODIFY を推定する(WI-334)
|
|
83
|
+
return ChangedFile.create({
|
|
84
|
+
filePath: p,
|
|
85
|
+
changeKind: await this.inferChangeKind(p),
|
|
86
|
+
beforeContent: null,
|
|
87
|
+
afterContent: null,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const beforeContent = targetChange?.beforeContent ?? null;
|
|
91
|
+
const afterContent = targetChange?.afterContent ?? null;
|
|
92
|
+
return ChangedFile.create({
|
|
93
|
+
filePath: p,
|
|
94
|
+
// 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
|
|
95
|
+
// 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
|
|
96
|
+
// NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
|
|
97
|
+
changeKind: beforeContent === null && afterContent !== null ? "CREATE" : "MODIFY",
|
|
98
|
+
beforeContent,
|
|
99
|
+
afterContent,
|
|
100
|
+
});
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
65
103
|
|
|
66
104
|
const classification = this.judgmentEngine.classify(changedFiles, config);
|
|
67
105
|
const eligibility = this.judgmentEngine.judge(changedFiles, config);
|
|
@@ -76,7 +114,7 @@ export class ClassifyChangeCategoryUseCase {
|
|
|
76
114
|
categoryForFile = categoryKey;
|
|
77
115
|
}
|
|
78
116
|
});
|
|
79
|
-
perFile.push({ path, category: categoryForFile ??
|
|
117
|
+
perFile.push({ path, category: categoryForFile ?? "unknown" });
|
|
80
118
|
}
|
|
81
119
|
|
|
82
120
|
const fullModeRequired = !eligibility.isEligible();
|
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
* quick-mode ユニットの Composition Root
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { CheckChangeCategoryHandler } from
|
|
8
|
+
import { QuickModeDecisionContractMapper } from "./application/mappers/quick-mode-decision-contract-mapper.js";
|
|
9
|
+
import { BuildRelaxationProfileUseCase } from "./application/usecases/build-relaxation-profile-usecase.js";
|
|
10
|
+
import { ClassifyChangeCategoryUseCase } from "./application/usecases/classify-change-category-usecase.js";
|
|
11
|
+
import { ExecuteQuickCiCheckUseCase } from "./application/usecases/execute-quick-ci-check-usecase.js";
|
|
12
|
+
import { JudgeQuickModeEligibilityUseCase } from "./application/usecases/judge-quick-mode-eligibility-usecase.js";
|
|
13
|
+
import { QuickModeJudgmentEngine } from "./domain/services/quick-mode-judgment-engine.js";
|
|
14
|
+
import { ValidatorRelaxationService } from "./domain/services/validator-relaxation-service.js";
|
|
15
|
+
import { FsFileExistenceAdapter } from "./infrastructure/adapters/fs-file-existence-adapter.js";
|
|
16
|
+
import { GitDiffChangedFilesAdapter } from "./infrastructure/adapters/git-diff-changed-files-adapter.js";
|
|
17
|
+
import { HarnessConfigQuickModeConfigAdapter } from "./infrastructure/adapters/harness-config-quick-mode-config-adapter.js";
|
|
18
|
+
import { ValidatorSystemQuickModeExecutionAdapter } from "./infrastructure/adapters/validator-system-quick-mode-execution-adapter.js";
|
|
19
|
+
import { ValidatorSystemValidatorIdRegistryAdapter } from "./infrastructure/adapters/validator-system-validator-id-registry-adapter.js";
|
|
20
|
+
import { CheckChangeCategoryHandler } from "./presentation/handlers/check-change-category-handler.js";
|
|
21
|
+
import { CiCheckQuickModeHandler } from "./presentation/handlers/ci-check-quick-mode-handler.js";
|
|
21
22
|
|
|
22
23
|
export interface QuickModeCompositionRoot {
|
|
23
24
|
handler: CiCheckQuickModeHandler;
|
|
@@ -28,12 +29,20 @@ export interface QuickModeCompositionRoot {
|
|
|
28
29
|
classifyUseCase: ClassifyChangeCategoryUseCase;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export
|
|
32
|
+
export interface QuickModeCompositionRootOptions {
|
|
33
|
+
configPath?: string;
|
|
34
|
+
rootDir?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createQuickModeCompositionRoot(
|
|
38
|
+
options: QuickModeCompositionRootOptions = {},
|
|
39
|
+
): QuickModeCompositionRoot {
|
|
32
40
|
// Infrastructure
|
|
33
41
|
const gitDiffAdapter = new GitDiffChangedFilesAdapter();
|
|
34
|
-
const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter();
|
|
42
|
+
const harnessConfigAdapter = new HarnessConfigQuickModeConfigAdapter(options.configPath);
|
|
35
43
|
const validatorIdRegistryAdapter = new ValidatorSystemValidatorIdRegistryAdapter();
|
|
36
44
|
const validatorExecutionPort = new ValidatorSystemQuickModeExecutionAdapter();
|
|
45
|
+
const fileExistencePort = new FsFileExistenceAdapter(options.rootDir);
|
|
37
46
|
|
|
38
47
|
// Domain Services
|
|
39
48
|
const judgmentEngine = new QuickModeJudgmentEngine();
|
|
@@ -72,6 +81,7 @@ export function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
|
|
|
72
81
|
const classifyUseCase = new ClassifyChangeCategoryUseCase({
|
|
73
82
|
quickModeConfigPort,
|
|
74
83
|
judgmentEngine,
|
|
84
|
+
fileExistencePort,
|
|
75
85
|
});
|
|
76
86
|
|
|
77
87
|
// Presentation
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-372
|
|
5
|
+
*
|
|
6
|
+
* QuickMode 設定の不変条件違反を表すエラー。
|
|
7
|
+
*
|
|
8
|
+
* `quick-mode-config.ts` と `category-override-rules.ts` の双方から参照されるため、
|
|
9
|
+
* 循環 import を避ける目的で独立ファイルに切り出している。
|
|
10
|
+
* 後方互換のため `quick-mode-config.ts` からも re-export される。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class QuickModeConfigError extends Error {
|
|
14
|
+
constructor(message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'QuickModeConfigError';
|
|
17
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit quick-mode
|
|
4
4
|
* @work-item-id WI-204
|
|
5
|
+
* @work-item-id WI-349
|
|
6
|
+
* @work-item-id WI-352
|
|
7
|
+
* @work-item-id WI-372
|
|
5
8
|
*
|
|
6
9
|
* ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
|
|
7
10
|
*/
|
|
8
11
|
|
|
12
|
+
import { CategoryOverrideRules } from "../value-objects/category-override-rules.js";
|
|
9
13
|
import { ChangeCategory } from "../value-objects/change-category.js";
|
|
10
14
|
import { ChangeClassification } from "../value-objects/change-classification.js";
|
|
11
15
|
import type { ChangedFile } from "../value-objects/changed-file.js";
|
|
@@ -13,18 +17,38 @@ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
|
|
|
13
17
|
import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
|
|
14
18
|
import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
|
|
15
19
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
// config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
|
|
21
|
+
// これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
|
|
22
|
+
// 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
|
|
23
|
+
// ワイルドカード(.github/** 等)には広げず、実在する bootstrap ファイル名のみを
|
|
24
|
+
// 列挙して fail-closed を維持する(WI-334 の .github/workflows、
|
|
25
|
+
// WI-261 の skills/**/*.md と同型の判断)。
|
|
26
|
+
// package.json は protected-file 経路で別途保護されるためここには含めない。
|
|
27
|
+
// @work-item-id WI-352
|
|
28
|
+
const ROOT_BOOTSTRAP_CONFIG_FILES: ReadonlySet<string> = new Set([
|
|
29
|
+
".gitignore",
|
|
30
|
+
".gitattributes",
|
|
31
|
+
".editorconfig",
|
|
32
|
+
".npmrc",
|
|
33
|
+
".nvmrc",
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const HUSKY_DIRECTORY_PREFIX = ".husky/";
|
|
38
|
+
|
|
39
|
+
function isRootBootstrapConfigFile(filePath: string): boolean {
|
|
40
|
+
if (ROOT_BOOTSTRAP_CONFIG_FILES.has(filePath)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// tsconfig.*.json(tsconfig.build.json / tsconfig.test.json 等)もルート直下のみ許可
|
|
44
|
+
if (/^tsconfig\.[^/]+\.json$/.test(filePath)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// .husky/ 配下の hook スクリプト(L0 の runtime 定義)
|
|
48
|
+
return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function categorizeFileByBuiltInRules(file: ChangedFile): ChangeCategory {
|
|
28
52
|
const { filePath, changeKind } = file;
|
|
29
53
|
|
|
30
54
|
// Config files must stay config even when Edit payload snippets look like
|
|
@@ -37,6 +61,23 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
37
61
|
return ChangeCategory.fromString("config");
|
|
38
62
|
}
|
|
39
63
|
|
|
64
|
+
// config: .github/workflows/ 配下の CI workflow(.yml / .yaml)。
|
|
65
|
+
// CI workflow は unit を持たない構成ファイルであり、CREATE でも config に分類する。
|
|
66
|
+
// 内容レベルの防御は L3-006 injection scanner と integrity pin が担う
|
|
67
|
+
// (WI-261 の skills/**/*.md → docs 分類と同型の判断)。
|
|
68
|
+
// MODIFY がフォールバックで bugfix として quick を通過する現状との整合も取る。
|
|
69
|
+
// .github/ 全体には広げず workflows/ 配下の yml/yaml に限定し、
|
|
70
|
+
// それ以外の .github/ 配下は従来どおりフォールバック(fail-closed)を維持する。
|
|
71
|
+
// @work-item-id WI-334
|
|
72
|
+
if (filePath.startsWith(".github/workflows/") && (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))) {
|
|
73
|
+
return ChangeCategory.fromString("config");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// config: リポジトリ直下の bootstrap 設定ファイル(WI-352)
|
|
77
|
+
if (isRootBootstrapConfigFile(filePath)) {
|
|
78
|
+
return ChangeCategory.fromString("config");
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
if (isCommentOnlyDiff(file)) {
|
|
41
82
|
return ChangeCategory.fromString("docs");
|
|
42
83
|
}
|
|
@@ -80,18 +121,67 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
80
121
|
return ChangeCategory.fromString("bugfix");
|
|
81
122
|
}
|
|
82
123
|
|
|
124
|
+
// 組み込み判定が構造的に高リスク(domain / api)のカテゴリは override で降格できない。
|
|
125
|
+
// @work-item-id WI-372
|
|
126
|
+
const NON_DOWNGRADABLE_BUILT_IN_CATEGORIES: ReadonlySet<string> = new Set(["domain", "api"]);
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* WI-372: `quickMode.categoryOverrides` を反映したカテゴリ判定。
|
|
130
|
+
*
|
|
131
|
+
* override は「このパスはこの種類の変更である」という利用者の明示宣言なので、
|
|
132
|
+
* 組み込みルールより **先** に評価する(DD-1)。組み込みの後段に置くと
|
|
133
|
+
* `notes/x.config.json` のような偶発的な組み込みマッチに常に負け、
|
|
134
|
+
* 「設定したのに効かない」不可解な挙動になるため。
|
|
135
|
+
*
|
|
136
|
+
* ただし組み込み判定が `domain` / `api` のファイルは override で降格できない(DD-2)。
|
|
137
|
+
* `judge()` の NEW_DOMAIN は CREATE のみ、API_CONTRACT は port/adapter のみを見るため、
|
|
138
|
+
* ガードが無いと domain ファイルの MODIFY が `docs` として素通りしてしまう。
|
|
139
|
+
* 降格は禁じるが昇格(domain → api 等)は許す。
|
|
140
|
+
*
|
|
141
|
+
* override 未設定時は組み込み分類と完全に一致する(後方互換)。
|
|
142
|
+
*/
|
|
143
|
+
function categorizeFile(file: ChangedFile, overrides: CategoryOverrideRules): ChangeCategory {
|
|
144
|
+
const builtIn = categorizeFileByBuiltInRules(file);
|
|
145
|
+
if (overrides.isEmpty()) {
|
|
146
|
+
return builtIn;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const override = overrides.resolve(file.filePath);
|
|
150
|
+
if (override === null) {
|
|
151
|
+
return builtIn;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (NON_DOWNGRADABLE_BUILT_IN_CATEGORIES.has(builtIn.toString())) {
|
|
155
|
+
return override.riskPriority() > builtIn.riskPriority() ? override : builtIn;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return override;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
|
|
163
|
+
*
|
|
164
|
+
* 従来はパス列挙のみだったため「なぜそのパスが不許可なのか」が読み取れず、
|
|
165
|
+
* 利用者が「ワークツリーの無関係な変更のせいでブロックされている」と
|
|
166
|
+
* 誤認する原因になっていた(issue #41 症状②)。
|
|
167
|
+
*/
|
|
168
|
+
function describeChangedFile(file: ChangedFile, category: string): string {
|
|
169
|
+
return `${file.filePath} (category=${category}, changeKind=${file.changeKind})`;
|
|
170
|
+
}
|
|
171
|
+
|
|
83
172
|
export class QuickModeJudgmentEngine {
|
|
84
|
-
classify(changedFiles: readonly ChangedFile[],
|
|
173
|
+
classify(changedFiles: readonly ChangedFile[], config?: QuickModeConfig): ChangeClassification {
|
|
85
174
|
if (changedFiles.length === 0) {
|
|
86
175
|
return new ChangeClassification(null, new Map(), 0);
|
|
87
176
|
}
|
|
88
177
|
|
|
178
|
+
const overrides = config?.categoryOverrides ?? CategoryOverrideRules.empty();
|
|
89
179
|
const categorizedMap = new Map<string, ChangedFile[]>();
|
|
90
180
|
let dominantCategory: ChangeCategory | null = null;
|
|
91
181
|
let dominantPriority = -1;
|
|
92
182
|
|
|
93
183
|
for (const file of changedFiles) {
|
|
94
|
-
const category = categorizeFile(file);
|
|
184
|
+
const category = categorizeFile(file, overrides);
|
|
95
185
|
const key = category.toString();
|
|
96
186
|
|
|
97
187
|
if (!categorizedMap.has(key)) {
|
|
@@ -99,7 +189,7 @@ export class QuickModeJudgmentEngine {
|
|
|
99
189
|
}
|
|
100
190
|
categorizedMap.get(key)!.push(file);
|
|
101
191
|
|
|
102
|
-
const priority =
|
|
192
|
+
const priority = category.riskPriority();
|
|
103
193
|
if (priority > dominantPriority) {
|
|
104
194
|
dominantPriority = priority;
|
|
105
195
|
dominantCategory = category;
|
|
@@ -115,9 +205,13 @@ export class QuickModeJudgmentEngine {
|
|
|
115
205
|
// 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
|
|
116
206
|
if (config.isFullModeRequiredFor("mixedCategories")) {
|
|
117
207
|
const notAllowedFiles: ChangedFile[] = [];
|
|
208
|
+
const notAllowedDescriptions: string[] = [];
|
|
118
209
|
classification.categorizedFiles.forEach((files, categoryKey) => {
|
|
119
210
|
if (!config.isAllowed(categoryKey)) {
|
|
120
211
|
notAllowedFiles.push(...files);
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
notAllowedDescriptions.push(describeChangedFile(file, categoryKey));
|
|
214
|
+
}
|
|
121
215
|
}
|
|
122
216
|
});
|
|
123
217
|
|
|
@@ -125,7 +219,7 @@ export class QuickModeJudgmentEngine {
|
|
|
125
219
|
return QuickModeEligibility.rejected(
|
|
126
220
|
"MIXED_CHANGES",
|
|
127
221
|
notAllowedFiles,
|
|
128
|
-
`allowedCategories外のファイルが含まれています: ${
|
|
222
|
+
`allowedCategories外のファイルが含まれています: ${notAllowedDescriptions.join(", ")}`,
|
|
129
223
|
);
|
|
130
224
|
}
|
|
131
225
|
}
|
|
@@ -140,7 +234,9 @@ export class QuickModeJudgmentEngine {
|
|
|
140
234
|
return QuickModeEligibility.rejected(
|
|
141
235
|
"NEW_DOMAIN",
|
|
142
236
|
newDomainFiles,
|
|
143
|
-
`domain/配下に新規ファイルが追加されています: ${newDomainFiles
|
|
237
|
+
`domain/配下に新規ファイルが追加されています: ${newDomainFiles
|
|
238
|
+
.map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
|
|
239
|
+
.join(", ")}`,
|
|
144
240
|
);
|
|
145
241
|
}
|
|
146
242
|
}
|
|
@@ -155,7 +251,9 @@ export class QuickModeJudgmentEngine {
|
|
|
155
251
|
return QuickModeEligibility.rejected(
|
|
156
252
|
"API_CONTRACT",
|
|
157
253
|
apiContractFiles,
|
|
158
|
-
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
|
|
254
|
+
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
|
|
255
|
+
.map((f) => describeChangedFile(f, categorizeFile(f, config.categoryOverrides).toString()))
|
|
256
|
+
.join(", ")}`,
|
|
159
257
|
);
|
|
160
258
|
}
|
|
161
259
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-372
|
|
5
|
+
*
|
|
6
|
+
* `quickMode.categoryOverrides` を表す値オブジェクト。
|
|
7
|
+
* ChangeCategory 7 値をキー、glob パターン列を値とする写像で、
|
|
8
|
+
* プロジェクト固有パス(`results/**` 等)を任意カテゴリへ割り当てる。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { QuickModeConfigError } from '../errors/quick-mode-config-error.js';
|
|
12
|
+
import {
|
|
13
|
+
CHANGE_CATEGORY_VALUES,
|
|
14
|
+
ChangeCategory,
|
|
15
|
+
type ChangeCategoryValue,
|
|
16
|
+
isChangeCategoryValue,
|
|
17
|
+
} from './change-category.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* glob → 正規表現の変換。
|
|
21
|
+
*
|
|
22
|
+
* domain 層は repo 全体で外部 npm パッケージを import していないため
|
|
23
|
+
* (picomatch は infrastructure adapter 専用)、
|
|
24
|
+
* `agent-integration/domain/value-objects/protected-file-list.ts` と同型の
|
|
25
|
+
* 純正規表現実装を採る(logical_design.md LD-10)。
|
|
26
|
+
*
|
|
27
|
+
* サポートする記法:
|
|
28
|
+
* - `**` … `/` を含む任意の文字列
|
|
29
|
+
* - `*` … `/` を含まない任意の文字列
|
|
30
|
+
* - `?` … `/` 以外の 1 文字
|
|
31
|
+
* - その他はリテラル(正規表現メタ文字はエスケープ)
|
|
32
|
+
*/
|
|
33
|
+
function globToRegExp(pattern: string): RegExp {
|
|
34
|
+
const regexSource = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
.replace(/\*\*/g, '__DOUBLE_STAR__')
|
|
37
|
+
.replace(/\*/g, '[^/]*')
|
|
38
|
+
.replace(/__DOUBLE_STAR__/g, '.*')
|
|
39
|
+
.replace(/\?/g, '[^/]');
|
|
40
|
+
return new RegExp(`^${regexSource}$`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type CategoryOverrideRulesInput = Readonly<Record<string, readonly string[]>>;
|
|
44
|
+
|
|
45
|
+
export class CategoryOverrideRules {
|
|
46
|
+
private readonly patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>;
|
|
47
|
+
private readonly matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>;
|
|
48
|
+
|
|
49
|
+
private constructor(
|
|
50
|
+
patterns: ReadonlyMap<ChangeCategoryValue, readonly string[]>,
|
|
51
|
+
matchers: ReadonlyMap<ChangeCategoryValue, readonly RegExp[]>,
|
|
52
|
+
) {
|
|
53
|
+
this.patterns = patterns;
|
|
54
|
+
this.matchers = matchers;
|
|
55
|
+
Object.freeze(this);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static empty(): CategoryOverrideRules {
|
|
59
|
+
return new CategoryOverrideRules(new Map(), new Map());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
static create(raw: CategoryOverrideRulesInput | undefined | null): CategoryOverrideRules {
|
|
63
|
+
if (raw === undefined || raw === null) {
|
|
64
|
+
return CategoryOverrideRules.empty();
|
|
65
|
+
}
|
|
66
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
67
|
+
throw new QuickModeConfigError('categoryOverrides must be an object keyed by change category');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const patterns = new Map<ChangeCategoryValue, readonly string[]>();
|
|
71
|
+
const matchers = new Map<ChangeCategoryValue, readonly RegExp[]>();
|
|
72
|
+
|
|
73
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
74
|
+
if (!isChangeCategoryValue(key)) {
|
|
75
|
+
throw new QuickModeConfigError(
|
|
76
|
+
`categoryOverrides contains unknown category "${key}". Valid values: ${CHANGE_CATEGORY_VALUES.join(', ')}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(value)) {
|
|
80
|
+
throw new QuickModeConfigError(`categoryOverrides.${key} must be an array of glob patterns`);
|
|
81
|
+
}
|
|
82
|
+
for (const pattern of value) {
|
|
83
|
+
if (typeof pattern !== 'string' || pattern.length === 0) {
|
|
84
|
+
throw new QuickModeConfigError(
|
|
85
|
+
`categoryOverrides.${key} must not contain an empty or non-string glob pattern`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (value.length === 0) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
patterns.set(key, Object.freeze([...value]));
|
|
93
|
+
matchers.set(key, Object.freeze(value.map(globToRegExp)));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new CategoryOverrideRules(patterns, matchers);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isEmpty(): boolean {
|
|
100
|
+
return this.matchers.size === 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* パスに対応する override カテゴリを返す。
|
|
105
|
+
* 一致が無ければ null(組み込み分類へ委譲)。
|
|
106
|
+
* 複数カテゴリに一致した場合はリスク優先度が最も高いカテゴリを返すため、
|
|
107
|
+
* JSON のキー列挙順に依存しない決定的な結果になる(domain_model.md DD-4)。
|
|
108
|
+
*/
|
|
109
|
+
resolve(filePath: string): ChangeCategory | null {
|
|
110
|
+
if (filePath === '' || this.matchers.size === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let resolved: ChangeCategory | null = null;
|
|
115
|
+
let resolvedPriority = -1;
|
|
116
|
+
|
|
117
|
+
for (const [category, regexps] of this.matchers) {
|
|
118
|
+
if (!regexps.some((regexp) => regexp.test(filePath))) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const candidate = ChangeCategory.fromString(category);
|
|
122
|
+
const priority = candidate.riskPriority();
|
|
123
|
+
if (priority > resolvedPriority) {
|
|
124
|
+
resolvedPriority = priority;
|
|
125
|
+
resolved = candidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return resolved;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** キーは ChangeCategory 語彙順に正規化する(equals をキー順に依存させないため) */
|
|
133
|
+
toRecord(): Readonly<Record<string, readonly string[]>> {
|
|
134
|
+
const record: Record<string, readonly string[]> = {};
|
|
135
|
+
for (const category of CHANGE_CATEGORY_VALUES) {
|
|
136
|
+
const globs = this.patterns.get(category);
|
|
137
|
+
if (globs !== undefined) {
|
|
138
|
+
record[category] = globs;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Object.freeze(record);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
equals(other: CategoryOverrideRules): boolean {
|
|
145
|
+
return JSON.stringify(this.toRecord()) === JSON.stringify(other.toRecord());
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -24,6 +24,31 @@ const VALID_CATEGORIES: readonly ChangeCategoryValue[] = [
|
|
|
24
24
|
'api',
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* ChangeCategory の語彙。config の enum 検証や categoryOverrides の
|
|
29
|
+
* キー検証はこの定義を唯一の権威として参照する。
|
|
30
|
+
* @work-item-id WI-372
|
|
31
|
+
*/
|
|
32
|
+
export const CHANGE_CATEGORY_VALUES: readonly ChangeCategoryValue[] = VALID_CATEGORIES;
|
|
33
|
+
|
|
34
|
+
/** 与えられた文字列が ChangeCategory の語彙に含まれるか(正規化はしない) */
|
|
35
|
+
export function isChangeCategoryValue(raw: string): raw is ChangeCategoryValue {
|
|
36
|
+
return VALID_CATEGORIES.includes(raw as ChangeCategoryValue);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// リスク順優先度(api > domain > feature > bugfix > test > config > docs)。
|
|
40
|
+
// 複数の分類条件に一致した場合に、より高リスク側へ倒すための順序定義。
|
|
41
|
+
// @work-item-id WI-372
|
|
42
|
+
const RISK_PRIORITY: Record<ChangeCategoryValue, number> = {
|
|
43
|
+
api: 6,
|
|
44
|
+
domain: 5,
|
|
45
|
+
feature: 4,
|
|
46
|
+
bugfix: 3,
|
|
47
|
+
test: 2,
|
|
48
|
+
config: 1,
|
|
49
|
+
docs: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
27
52
|
export class UnknownChangeCategoryError extends Error {
|
|
28
53
|
constructor(raw: string) {
|
|
29
54
|
super(`Unknown change category: "${raw}". Valid values: ${VALID_CATEGORIES.join(', ')}`);
|
|
@@ -50,6 +75,14 @@ export class ChangeCategory {
|
|
|
50
75
|
return this.value === 'domain' || this.value === 'feature' || this.value === 'api';
|
|
51
76
|
}
|
|
52
77
|
|
|
78
|
+
/**
|
|
79
|
+
* リスク優先度。値が大きいほど高リスク(api=6 ... docs=0)。
|
|
80
|
+
* @work-item-id WI-372
|
|
81
|
+
*/
|
|
82
|
+
riskPriority(): number {
|
|
83
|
+
return RISK_PRIORITY[this.value];
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
toString(): string {
|
|
54
87
|
return this.value;
|
|
55
88
|
}
|