phasegate 0.264.0 → 0.315.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 +54 -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/guide/installation.md +1 -1
- package/docs/guide/layer-model.md +2 -0
- package/docs/guide/quick-vs-full-mode.md +28 -3
- package/docs/guide/troubleshooting.md +37 -0
- package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
- package/docs/templates/ci/aidlc-gate.yml +22 -4
- package/package.json +2 -2
- package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
- package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
- 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 +45 -27
- package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
- package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
- package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
- package/scripts/harness/ci-governance/composition-root.ts +2 -2
- package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
- package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
- package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
- package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
- package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
- package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
- package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
- package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
- package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
- package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
- package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
- 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 +234 -53
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
- 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/deployment-manifest.ts +43 -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 +83 -15
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
- 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/services/quick-mode-judgment-engine.ts +72 -3
- package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
- 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 +32 -7
- package/scripts/harness/validator-system/composition-root.ts +4 -1
- package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
- 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/harness-config-validator-config-adapter.ts +89 -3
- package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
- 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/quick-implementor/SKILL.md +19 -0
|
@@ -3,20 +3,32 @@
|
|
|
3
3
|
* @unit phase-dependency-model
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFile } from
|
|
7
|
-
import * as path from
|
|
8
|
-
import type { PlanDocumentReaderPort } from
|
|
9
|
-
import type { PhaseNode } from
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { PlanDocumentReaderPort } from "../../domain/ports/plan-document-reader-port.js";
|
|
9
|
+
import type { PhaseNode } from "../../domain/values/phase-node.js";
|
|
10
|
+
import { PlanEvidence } from "../../domain/values/plan-evidence.js";
|
|
11
|
+
import type { PlanningMode } from "../../domain/values/planning-mode.js";
|
|
12
12
|
|
|
13
13
|
export interface MarkdownPlanDocumentReaderDeps {
|
|
14
14
|
readonly rootDir: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
/**
|
|
18
|
+
* WI-358 (issue #29): 見出しの表記ゆれで Planning Mode の evidence 判定が落ちる罠を潰す。
|
|
19
|
+
*
|
|
20
|
+
* 旧パターンは "QA" 綴りしか受け付けず、エージェントが自然に書く `## Q&A` /
|
|
21
|
+
* 全角の `## Q&A` を弾いていた。中身は同一なのに phase gate だけが落ち、
|
|
22
|
+
* 原因が見出しの綴りだと気付けないまま止まる。
|
|
23
|
+
*
|
|
24
|
+
* 緩和方向のみの変更であり、既存の `## QA` / `## 4. QA(不明点・確認事項)` は
|
|
25
|
+
* すべて従来どおりマッチする。
|
|
26
|
+
*/
|
|
27
|
+
const QA_SECTION_PATTERN = /^##+\s*(?:\d+[..]\s*)?Q[&&]?A\b/m;
|
|
28
|
+
const QUESTION_PATTERN = /^(?:Q:|###\s*\[Question\])/gm;
|
|
29
|
+
const LEGACY_ANSWER_PATTERN = /^A:/gm;
|
|
30
|
+
const STRUCTURED_ANSWER_PATTERN = /^\[Answer\][ \t]*$/gm;
|
|
31
|
+
const ANSWER_BODY_BOUNDARY_PATTERN = /^(?:###\s*\[Question\]|#{1,6}\s+)/m;
|
|
20
32
|
|
|
21
33
|
export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
22
34
|
private readonly rootDir: string;
|
|
@@ -55,7 +67,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
55
67
|
|
|
56
68
|
let content: string;
|
|
57
69
|
try {
|
|
58
|
-
content = await readFile(absolutePath,
|
|
70
|
+
content = await readFile(absolutePath, "utf8");
|
|
59
71
|
} catch {
|
|
60
72
|
return PlanEvidence.create({
|
|
61
73
|
exists: false,
|
|
@@ -66,11 +78,7 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
66
78
|
|
|
67
79
|
const hasQaSection = QA_SECTION_PATTERN.test(content);
|
|
68
80
|
const qaComplete = this.isQaComplete(content, hasQaSection, expectedMode);
|
|
69
|
-
const planningModeMatch = this.detectPlanningModeMatch(
|
|
70
|
-
content,
|
|
71
|
-
hasQaSection,
|
|
72
|
-
expectedMode,
|
|
73
|
-
);
|
|
81
|
+
const planningModeMatch = this.detectPlanningModeMatch(content, hasQaSection, expectedMode);
|
|
74
82
|
|
|
75
83
|
return PlanEvidence.create({
|
|
76
84
|
exists: true,
|
|
@@ -79,16 +87,12 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
79
87
|
});
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
private isQaComplete(
|
|
83
|
-
content: string,
|
|
84
|
-
hasQaSection: boolean,
|
|
85
|
-
expectedMode: PlanningMode,
|
|
86
|
-
): boolean {
|
|
90
|
+
private isQaComplete(content: string, hasQaSection: boolean, expectedMode: PlanningMode): boolean {
|
|
87
91
|
if (expectedMode.requiresAnsweredQa()) {
|
|
88
92
|
if (!hasQaSection) return false;
|
|
89
93
|
const qaSection = this.extractQaSection(content);
|
|
90
94
|
const questionCount = (qaSection.match(QUESTION_PATTERN) ?? []).length;
|
|
91
|
-
const answerCount = (qaSection
|
|
95
|
+
const answerCount = this.countAnswers(qaSection);
|
|
92
96
|
return questionCount > 0 && questionCount === answerCount;
|
|
93
97
|
}
|
|
94
98
|
|
|
@@ -99,16 +103,12 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
99
103
|
return true;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
private detectPlanningModeMatch(
|
|
103
|
-
content: string,
|
|
104
|
-
hasQaSection: boolean,
|
|
105
|
-
expectedMode: PlanningMode,
|
|
106
|
-
): boolean {
|
|
106
|
+
private detectPlanningModeMatch(content: string, hasQaSection: boolean, expectedMode: PlanningMode): boolean {
|
|
107
107
|
if (expectedMode.requiresAnsweredQa()) {
|
|
108
108
|
if (!hasQaSection) return false;
|
|
109
109
|
const qaSection = this.extractQaSection(content);
|
|
110
110
|
const questionCount = (qaSection.match(QUESTION_PATTERN) ?? []).length;
|
|
111
|
-
const answerCount = (qaSection
|
|
111
|
+
const answerCount = this.countAnswers(qaSection);
|
|
112
112
|
return questionCount > 0 && answerCount === questionCount;
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -121,14 +121,42 @@ export class MarkdownPlanDocumentReader implements PlanDocumentReaderPort {
|
|
|
121
121
|
|
|
122
122
|
private extractQaSection(content: string): string {
|
|
123
123
|
const match = content.match(QA_SECTION_PATTERN);
|
|
124
|
-
if (!match || match.index === undefined) return
|
|
124
|
+
if (!match || match.index === undefined) return "";
|
|
125
125
|
|
|
126
126
|
const start = match.index;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
const sectionContentStart = start + match[0].length;
|
|
128
|
+
const sectionContent = content.slice(sectionContentStart);
|
|
129
|
+
const headingMarker = match[0].match(/^##+/)?.[0];
|
|
130
|
+
if (!headingMarker) return "";
|
|
131
|
+
|
|
132
|
+
const sameLevelHeadingPattern = new RegExp(`^${headingMarker}(?!#)[ \\t]+(.+)$`, "gm");
|
|
133
|
+
let end = content.length;
|
|
134
|
+
for (const headingMatch of sectionContent.matchAll(sameLevelHeadingPattern)) {
|
|
135
|
+
if (/^\[Question\](?:\s|$)/.test(headingMatch[1].trimStart())) continue;
|
|
136
|
+
if (headingMatch.index !== undefined) {
|
|
137
|
+
end = sectionContentStart + headingMatch.index;
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
131
141
|
|
|
132
142
|
return content.slice(start, end);
|
|
133
143
|
}
|
|
144
|
+
|
|
145
|
+
private countAnswers(qaSection: string): number {
|
|
146
|
+
const legacyAnswerCount = (qaSection.match(LEGACY_ANSWER_PATTERN) ?? []).length;
|
|
147
|
+
let structuredAnswerCount = 0;
|
|
148
|
+
|
|
149
|
+
for (const answerMatch of qaSection.matchAll(STRUCTURED_ANSWER_PATTERN)) {
|
|
150
|
+
if (answerMatch.index === undefined) continue;
|
|
151
|
+
const bodyStart = answerMatch.index + answerMatch[0].length;
|
|
152
|
+
const followingContent = qaSection.slice(bodyStart);
|
|
153
|
+
const boundaryMatch = followingContent.match(ANSWER_BODY_BOUNDARY_PATTERN);
|
|
154
|
+
const answerBody =
|
|
155
|
+
boundaryMatch?.index === undefined ? followingContent : followingContent.slice(0, boundaryMatch.index);
|
|
156
|
+
const hasBody = answerBody.split(/\r?\n/).some((line) => line.trim().length > 0);
|
|
157
|
+
if (hasBody) structuredAnswerCount += 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return legacyAnswerCount + structuredAnswerCount;
|
|
161
|
+
}
|
|
134
162
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @layer presentation
|
|
3
3
|
* @unit phase2-extensions
|
|
4
4
|
*/
|
|
5
|
-
import type { CheckDocFreshnessUseCase } from
|
|
6
|
-
import { FreshnessResultFormatter } from
|
|
5
|
+
import type { CheckDocFreshnessUseCase } from "../../application/usecases/check-doc-freshness-usecase.js";
|
|
6
|
+
import { FreshnessResultFormatter } from "../formatters/freshness-result-formatter.js";
|
|
7
7
|
|
|
8
8
|
export class CheckFreshnessHandler {
|
|
9
9
|
private readonly formatter = new FreshnessResultFormatter();
|
|
@@ -12,26 +12,34 @@ export class CheckFreshnessHandler {
|
|
|
12
12
|
|
|
13
13
|
async handle(args: string[]): Promise<{ exitCode: number; stdout: string }> {
|
|
14
14
|
let targetPattern: string | undefined;
|
|
15
|
-
let format:
|
|
15
|
+
let format: "text" | "json" = "text";
|
|
16
16
|
let dryRun = false;
|
|
17
17
|
|
|
18
18
|
for (let index = 0; index < args.length; index += 1) {
|
|
19
19
|
const arg = args[index];
|
|
20
|
-
if (arg ===
|
|
20
|
+
if (arg === "--pattern") {
|
|
21
21
|
targetPattern = args[index + 1];
|
|
22
22
|
index += 1;
|
|
23
|
-
} else if (arg ===
|
|
24
|
-
format = (args[index + 1] as
|
|
23
|
+
} else if (arg === "--format") {
|
|
24
|
+
format = (args[index + 1] as "text" | "json") ?? "text";
|
|
25
25
|
index += 1;
|
|
26
|
-
} else if (arg ===
|
|
26
|
+
} else if (arg === "--dry-run") {
|
|
27
27
|
dryRun = true;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const result = await this.useCase.execute({ targetPattern, format, dryRun });
|
|
32
|
+
// WI-359: `--dry-run` は「副作用なしで診断のみ」(skills/doc-health-checker/SKILL.md)であり、
|
|
33
|
+
// シナリオ仕様 SC-P2-002 の期待結果も exit 0 である。従来は dryRun をパースだけして
|
|
34
|
+
// 捨てており、error 閾値超過の文書が 1 件でもあると診断のみの実行でも exit 1 になっていた。
|
|
35
|
+
// その結果 SC-P2-002 の e2e は「コードを一切変えなくても暦の経過だけで落ちる」状態になっていた。
|
|
36
|
+
// 実運用の鮮度ゲートは L4-004(`validate --layer L4`)が担い、そちらは本 handler を
|
|
37
|
+
// 経由しないため、dry-run を report-only にしてもゲートは緩まない。
|
|
38
|
+
// フラグ未指定時の exit 1 は従来どおり維持する。
|
|
39
|
+
const exitCode = dryRun ? 0 : result.summary.error > 0 ? 1 : 0;
|
|
32
40
|
return {
|
|
33
|
-
exitCode
|
|
34
|
-
stdout: format ===
|
|
41
|
+
exitCode,
|
|
42
|
+
stdout: format === "json" ? this.formatter.formatJson(result) : this.formatter.formatText(result),
|
|
35
43
|
};
|
|
36
44
|
}
|
|
37
45
|
}
|
|
@@ -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
|
|
@@ -2,6 +2,8 @@
|
|
|
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
|
|
5
7
|
*
|
|
6
8
|
* ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
|
|
7
9
|
*/
|
|
@@ -24,6 +26,37 @@ const RISK_PRIORITY: Record<string, number> = {
|
|
|
24
26
|
docs: 0,
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
// config: リポジトリ直下の bootstrap 設定ファイル(列挙型 allowlist)。
|
|
30
|
+
// これらは unit を持たず、CREATE だと feature に落ちて allowedCategories に
|
|
31
|
+
// 入れる手段がないため greenfield な初期セットアップが恒久的にブロックされていた。
|
|
32
|
+
// ワイルドカード(.github/** 等)には広げず、実在する bootstrap ファイル名のみを
|
|
33
|
+
// 列挙して fail-closed を維持する(WI-334 の .github/workflows、
|
|
34
|
+
// WI-261 の skills/**/*.md と同型の判断)。
|
|
35
|
+
// package.json は protected-file 経路で別途保護されるためここには含めない。
|
|
36
|
+
// @work-item-id WI-352
|
|
37
|
+
const ROOT_BOOTSTRAP_CONFIG_FILES: ReadonlySet<string> = new Set([
|
|
38
|
+
".gitignore",
|
|
39
|
+
".gitattributes",
|
|
40
|
+
".editorconfig",
|
|
41
|
+
".npmrc",
|
|
42
|
+
".nvmrc",
|
|
43
|
+
"tsconfig.json",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const HUSKY_DIRECTORY_PREFIX = ".husky/";
|
|
47
|
+
|
|
48
|
+
function isRootBootstrapConfigFile(filePath: string): boolean {
|
|
49
|
+
if (ROOT_BOOTSTRAP_CONFIG_FILES.has(filePath)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
// tsconfig.*.json(tsconfig.build.json / tsconfig.test.json 等)もルート直下のみ許可
|
|
53
|
+
if (/^tsconfig\.[^/]+\.json$/.test(filePath)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// .husky/ 配下の hook スクリプト(L0 の runtime 定義)
|
|
57
|
+
return filePath.startsWith(HUSKY_DIRECTORY_PREFIX) && filePath.slice(HUSKY_DIRECTORY_PREFIX.length).length > 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
28
61
|
const { filePath, changeKind } = file;
|
|
29
62
|
|
|
@@ -37,6 +70,23 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
37
70
|
return ChangeCategory.fromString("config");
|
|
38
71
|
}
|
|
39
72
|
|
|
73
|
+
// config: .github/workflows/ 配下の CI workflow(.yml / .yaml)。
|
|
74
|
+
// CI workflow は unit を持たない構成ファイルであり、CREATE でも config に分類する。
|
|
75
|
+
// 内容レベルの防御は L3-006 injection scanner と integrity pin が担う
|
|
76
|
+
// (WI-261 の skills/**/*.md → docs 分類と同型の判断)。
|
|
77
|
+
// MODIFY がフォールバックで bugfix として quick を通過する現状との整合も取る。
|
|
78
|
+
// .github/ 全体には広げず workflows/ 配下の yml/yaml に限定し、
|
|
79
|
+
// それ以外の .github/ 配下は従来どおりフォールバック(fail-closed)を維持する。
|
|
80
|
+
// @work-item-id WI-334
|
|
81
|
+
if (filePath.startsWith(".github/workflows/") && (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))) {
|
|
82
|
+
return ChangeCategory.fromString("config");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// config: リポジトリ直下の bootstrap 設定ファイル(WI-352)
|
|
86
|
+
if (isRootBootstrapConfigFile(filePath)) {
|
|
87
|
+
return ChangeCategory.fromString("config");
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
if (isCommentOnlyDiff(file)) {
|
|
41
91
|
return ChangeCategory.fromString("docs");
|
|
42
92
|
}
|
|
@@ -80,6 +130,17 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
|
|
|
80
130
|
return ChangeCategory.fromString("bugfix");
|
|
81
131
|
}
|
|
82
132
|
|
|
133
|
+
/**
|
|
134
|
+
* WI-349: 遮断理由に判定根拠(分類カテゴリと変更種別)を含める。
|
|
135
|
+
*
|
|
136
|
+
* 従来はパス列挙のみだったため「なぜそのパスが不許可なのか」が読み取れず、
|
|
137
|
+
* 利用者が「ワークツリーの無関係な変更のせいでブロックされている」と
|
|
138
|
+
* 誤認する原因になっていた(issue #41 症状②)。
|
|
139
|
+
*/
|
|
140
|
+
function describeChangedFile(file: ChangedFile, category: string): string {
|
|
141
|
+
return `${file.filePath} (category=${category}, changeKind=${file.changeKind})`;
|
|
142
|
+
}
|
|
143
|
+
|
|
83
144
|
export class QuickModeJudgmentEngine {
|
|
84
145
|
classify(changedFiles: readonly ChangedFile[], _config?: QuickModeConfig): ChangeClassification {
|
|
85
146
|
if (changedFiles.length === 0) {
|
|
@@ -115,9 +176,13 @@ export class QuickModeJudgmentEngine {
|
|
|
115
176
|
// 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
|
|
116
177
|
if (config.isFullModeRequiredFor("mixedCategories")) {
|
|
117
178
|
const notAllowedFiles: ChangedFile[] = [];
|
|
179
|
+
const notAllowedDescriptions: string[] = [];
|
|
118
180
|
classification.categorizedFiles.forEach((files, categoryKey) => {
|
|
119
181
|
if (!config.isAllowed(categoryKey)) {
|
|
120
182
|
notAllowedFiles.push(...files);
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
notAllowedDescriptions.push(describeChangedFile(file, categoryKey));
|
|
185
|
+
}
|
|
121
186
|
}
|
|
122
187
|
});
|
|
123
188
|
|
|
@@ -125,7 +190,7 @@ export class QuickModeJudgmentEngine {
|
|
|
125
190
|
return QuickModeEligibility.rejected(
|
|
126
191
|
"MIXED_CHANGES",
|
|
127
192
|
notAllowedFiles,
|
|
128
|
-
`allowedCategories外のファイルが含まれています: ${
|
|
193
|
+
`allowedCategories外のファイルが含まれています: ${notAllowedDescriptions.join(", ")}`,
|
|
129
194
|
);
|
|
130
195
|
}
|
|
131
196
|
}
|
|
@@ -140,7 +205,9 @@ export class QuickModeJudgmentEngine {
|
|
|
140
205
|
return QuickModeEligibility.rejected(
|
|
141
206
|
"NEW_DOMAIN",
|
|
142
207
|
newDomainFiles,
|
|
143
|
-
`domain/配下に新規ファイルが追加されています: ${newDomainFiles
|
|
208
|
+
`domain/配下に新規ファイルが追加されています: ${newDomainFiles
|
|
209
|
+
.map((f) => describeChangedFile(f, categorizeFile(f).toString()))
|
|
210
|
+
.join(", ")}`,
|
|
144
211
|
);
|
|
145
212
|
}
|
|
146
213
|
}
|
|
@@ -155,7 +222,9 @@ export class QuickModeJudgmentEngine {
|
|
|
155
222
|
return QuickModeEligibility.rejected(
|
|
156
223
|
"API_CONTRACT",
|
|
157
224
|
apiContractFiles,
|
|
158
|
-
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
|
|
225
|
+
`Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles
|
|
226
|
+
.map((f) => describeChangedFile(f, categorizeFile(f).toString()))
|
|
227
|
+
.join(", ")}`,
|
|
159
228
|
);
|
|
160
229
|
}
|
|
161
230
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer infrastructure
|
|
3
|
+
* @unit quick-mode
|
|
4
|
+
* @work-item-id WI-334
|
|
5
|
+
*
|
|
6
|
+
* FileExistencePort の fs 実装。
|
|
7
|
+
* 相対パスは baseDir(既定: process.cwd())基準で解決する。
|
|
8
|
+
* stat が ENOENT / ENOTDIR 以外で失敗した場合(権限エラー等)は
|
|
9
|
+
* 「存在する」扱いにして呼び出し側の changeKind 推定を MODIFY 既定
|
|
10
|
+
* (従来どおりの安全側)に倒す。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs/promises";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { FileExistencePort } from "../../application/ports/file-existence-port.js";
|
|
16
|
+
|
|
17
|
+
export class FsFileExistenceAdapter implements FileExistencePort {
|
|
18
|
+
private readonly baseDir: string;
|
|
19
|
+
|
|
20
|
+
constructor(baseDir?: string) {
|
|
21
|
+
this.baseDir = baseDir ?? process.cwd();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async exists(filePath: string): Promise<boolean> {
|
|
25
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(this.baseDir, filePath);
|
|
26
|
+
try {
|
|
27
|
+
await fs.stat(absolutePath);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const error = err as NodeJS.ErrnoException;
|
|
31
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
// stat 失敗(権限等)は存在扱い → changeKind は従来どおり MODIFY 既定(安全側)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|