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.
Files changed (92) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/docs/ADR/017-warning-severity-aggregation.md +17 -0
  3. package/docs/ADR/038-config-state-operation-permission-policy.md +78 -0
  4. package/docs/guide/installation.md +1 -1
  5. package/docs/guide/layer-model.md +2 -0
  6. package/docs/guide/quick-vs-full-mode.md +28 -3
  7. package/docs/guide/troubleshooting.md +37 -0
  8. package/docs/templates/agent-context/CLAUDE.md.template.md +6 -6
  9. package/docs/templates/ci/aidlc-gate.yml +22 -4
  10. package/package.json +2 -2
  11. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +98 -18
  12. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +25 -3
  13. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-full-mode-session-query-adapter.ts +75 -23
  14. package/scripts/harness/agent-integration/infrastructure/adapters/harness-config-config-query-adapter.ts +45 -27
  15. package/scripts/harness/agent-integration/presentation/post-tool-use-hook.ts +29 -16
  16. package/scripts/harness/agent-integration/presentation/pre-tool-use-hook.ts +51 -6
  17. package/scripts/harness/agent-integration/presentation/stop-hook.ts +35 -26
  18. package/scripts/harness/ci-governance/composition-root.ts +2 -2
  19. package/scripts/harness/ci-governance/domain/services/claude-md-composer.ts +20 -11
  20. package/scripts/harness/ci-governance/presentation/handlers/check-repetition-handler.ts +10 -2
  21. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +5 -1
  22. package/scripts/harness/config-foundation/domain/harness-config.ts +10 -7
  23. package/scripts/harness/config-foundation/domain/services/preset-resolution-service.ts +4 -1
  24. package/scripts/harness/config-foundation/domain/value-objects/project-config.ts +34 -18
  25. package/scripts/harness/config-foundation/infrastructure/presets/minimal.json +1 -1
  26. package/scripts/harness/config-foundation/infrastructure/presets/standard.json +1 -1
  27. package/scripts/harness/config-foundation/infrastructure/presets/strict.json +1 -1
  28. package/scripts/harness/config-foundation/infrastructure/repositories/file-system-config-repository.ts +27 -18
  29. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +1 -8
  30. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -8
  31. package/scripts/harness/harness-api/domain/ports/config-query-port.ts +11 -1
  32. package/scripts/harness/harness-api/domain/services/command-dispatch-service.ts +135 -87
  33. package/scripts/harness/harness-api/domain/services/status-derivation-service.ts +31 -21
  34. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +2 -22
  35. package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +23 -8
  36. package/scripts/harness/harness-api/infrastructure/adapters/biome-ast-engine-lint-adapter.ts +4 -4
  37. package/scripts/harness/harness-api/infrastructure/adapters/harness-config-query-adapter.ts +79 -34
  38. package/scripts/harness/harness-error/application/dto/create-harness-error-input.ts +3 -1
  39. package/scripts/harness/harness-error/application/dto/harness-error-contract.ts +3 -1
  40. package/scripts/harness/harness-error/application/mappers/harness-error-contract-mapper.ts +9 -17
  41. package/scripts/harness/harness-error/application/usecases/create-harness-error-use-case.ts +7 -7
  42. package/scripts/harness/harness-error/domain/services/harness-error-factory.ts +30 -33
  43. package/scripts/harness/harness-error/domain/value-objects/error-definition.ts +28 -17
  44. package/scripts/harness/harness-error/domain/value-objects/harness-error.ts +30 -9
  45. package/scripts/harness/harness-error/domain/value-objects/remediation-type.ts +30 -0
  46. package/scripts/harness/harness-error/infrastructure/registry/l2-error-definitions.ts +44 -29
  47. package/scripts/harness/harness-error/infrastructure/registry/l3-error-definitions.ts +44 -27
  48. package/scripts/harness/harness-error/infrastructure/registry/l4-error-definitions.ts +47 -32
  49. package/scripts/harness/installation/application/checks/claude-context-missing-check.ts +13 -7
  50. package/scripts/harness/installation/application/checks/config-status-check.ts +52 -0
  51. package/scripts/harness/installation/application/checks/husky-pre-commit-missing-check.ts +6 -0
  52. package/scripts/harness/installation/application/ports/config-status-probe-port.ts +9 -0
  53. package/scripts/harness/installation/application/usecases/run-doctor-diagnostics.ts +30 -8
  54. package/scripts/harness/installation/application/usecases/run-install.ts +234 -53
  55. package/scripts/harness/installation/application/usecases/run-reconcile.ts +311 -70
  56. package/scripts/harness/installation/composition-root.ts +13 -3
  57. package/scripts/harness/installation/domain/check-id.ts +2 -0
  58. package/scripts/harness/installation/domain/config-status.ts +17 -0
  59. package/scripts/harness/installation/domain/deployment-manifest.ts +43 -0
  60. package/scripts/harness/installation/domain/ports/heuristic-check.ts +10 -1
  61. package/scripts/harness/installation/infrastructure/adapters/config-status-probe-adapter.ts +79 -0
  62. package/scripts/harness/installation/presentation/cli/doctor-handler.ts +6 -1
  63. package/scripts/harness/installation/presentation/formatters/diagnostic-report-formatter.ts +19 -5
  64. package/scripts/harness/integrations/pre-commit.ts +17 -3
  65. package/scripts/harness/main.ts +83 -15
  66. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/markdown-plan-document-reader.ts +60 -32
  67. package/scripts/harness/phase2-extensions/presentation/handlers/check-freshness-handler.ts +17 -9
  68. package/scripts/harness/quick-mode/application/ports/file-existence-port.ts +15 -0
  69. package/scripts/harness/quick-mode/application/usecases/classify-change-category-usecase.ts +60 -22
  70. package/scripts/harness/quick-mode/composition-root.ts +25 -15
  71. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +72 -3
  72. package/scripts/harness/quick-mode/infrastructure/adapters/fs-file-existence-adapter.ts +38 -0
  73. package/scripts/harness/skill-quality/infrastructure/adapters/file-system-requirement-test-matrix-adapter.ts +51 -8
  74. package/scripts/harness/skill-quality/presentation/handlers/check-coverage-handler.ts +13 -6
  75. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +5 -1
  76. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +6 -1
  77. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-status-gateway.ts +15 -2
  78. package/scripts/harness/validator-system/application/use-cases/aggregate-validation-results-usecase.ts +11 -14
  79. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +32 -7
  80. package/scripts/harness/validator-system/composition-root.ts +4 -1
  81. package/scripts/harness/validator-system/domain/ports/ac-coverage-policy-port.ts +10 -1
  82. package/scripts/harness/validator-system/domain/services/effective-severity-policy.ts +39 -0
  83. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +6 -4
  84. package/scripts/harness/validator-system/domain/value-objects/drift-report.ts +12 -7
  85. package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +11 -3
  86. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +20 -17
  87. package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +89 -3
  88. package/scripts/harness/validator-system/infrastructure/adapters/nyquist-ac-coverage-policy-adapter.ts +49 -20
  89. package/scripts/harness/validator-system/infrastructure/adapters/phase-dependency-phase-gate-policy-adapter.ts +35 -14
  90. package/scripts/harness/validator-system/infrastructure/adapters/traceability-metadata-policy-adapter.ts +13 -8
  91. package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +19 -10
  92. 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 '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 type { PlanningMode } from '../../domain/values/planning-mode.js';
11
- import { PlanEvidence } from '../../domain/values/plan-evidence.js';
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
- const QA_SECTION_PATTERN = /^## QA\b/m;
18
- const QUESTION_PATTERN = /^Q:/gm;
19
- const ANSWER_PATTERN = /^A:/gm;
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, 'utf8');
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.match(ANSWER_PATTERN) ?? []).length;
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.match(ANSWER_PATTERN) ?? []).length;
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 nextSectionMatch = content.slice(start + match[0].length).match(/^## /m);
128
- const end = nextSectionMatch?.index
129
- ? start + match[0].length + nextSectionMatch.index
130
- : content.length;
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 '../../application/usecases/check-doc-freshness-usecase.js';
6
- import { FreshnessResultFormatter } from '../formatters/freshness-result-formatter.js';
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: 'text' | 'json' = 'text';
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 === '--pattern') {
20
+ if (arg === "--pattern") {
21
21
  targetPattern = args[index + 1];
22
22
  index += 1;
23
- } else if (arg === '--format') {
24
- format = (args[index + 1] as 'text' | 'json') ?? 'text';
23
+ } else if (arg === "--format") {
24
+ format = (args[index + 1] as "text" | "json") ?? "text";
25
25
  index += 1;
26
- } else if (arg === '--dry-run') {
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: result.summary.error > 0 ? 1 : 0,
34
- stdout: format === 'json' ? this.formatter.formatJson(result) : this.formatter.formatText(result),
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 { ChangedFile } from '../../domain/value-objects/changed-file.js';
10
- import { QuickModeJudgmentEngine } from '../../domain/services/quick-mode-judgment-engine.js';
11
- import type { QuickModeConfigPort } from '../ports/quick-mode-config-port.js';
12
- import type { ChangeCategoryClassificationContract, ChangeCategoryPerFile } from '../dto/change-category-classification-contract.js';
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
- async execute(
38
- input: ClassifyChangeCategoryUseCaseInput
39
- ): Promise<Readonly<ChangeCategoryClassificationContract>> {
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 = input.paths.map((p) => {
52
- const targetChange = targetChanges.get(p);
53
- const beforeContent = targetChange?.beforeContent ?? null;
54
- const afterContent = targetChange?.afterContent ?? null;
55
- return ChangedFile.create({
56
- filePath: p,
57
- // 変更前の内容が無く変更後の内容がある場合は新規作成 (CREATE) とみなす。
58
- // 以前は無条件で MODIFY 固定だったため、新規 domain/ ファイルが
59
- // NEW_DOMAIN 判定を回避して quick mode をすり抜けていた。
60
- changeKind: beforeContent === null && afterContent !== null ? 'CREATE' : 'MODIFY',
61
- beforeContent,
62
- afterContent,
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 ?? 'unknown' });
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 { GitDiffChangedFilesAdapter } from './infrastructure/adapters/git-diff-changed-files-adapter.js';
9
- import { HarnessConfigQuickModeConfigAdapter } from './infrastructure/adapters/harness-config-quick-mode-config-adapter.js';
10
- import { ValidatorSystemValidatorIdRegistryAdapter } from './infrastructure/adapters/validator-system-validator-id-registry-adapter.js';
11
- import { ValidatorSystemQuickModeExecutionAdapter } from './infrastructure/adapters/validator-system-quick-mode-execution-adapter.js';
12
- import { QuickModeJudgmentEngine } from './domain/services/quick-mode-judgment-engine.js';
13
- import { ValidatorRelaxationService } from './domain/services/validator-relaxation-service.js';
14
- import { QuickModeDecisionContractMapper } from './application/mappers/quick-mode-decision-contract-mapper.js';
15
- import { JudgeQuickModeEligibilityUseCase } from './application/usecases/judge-quick-mode-eligibility-usecase.js';
16
- import { BuildRelaxationProfileUseCase } from './application/usecases/build-relaxation-profile-usecase.js';
17
- import { ExecuteQuickCiCheckUseCase } from './application/usecases/execute-quick-ci-check-usecase.js';
18
- import { ClassifyChangeCategoryUseCase } from './application/usecases/classify-change-category-usecase.js';
19
- import { CiCheckQuickModeHandler } from './presentation/handlers/ci-check-quick-mode-handler.js';
20
- import { CheckChangeCategoryHandler } from './presentation/handlers/check-change-category-handler.js';
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 function createQuickModeCompositionRoot(): QuickModeCompositionRoot {
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外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(", ")}`,
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.map((f) => f.filePath).join(", ")}`,
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.map((f) => f.filePath).join(", ")}`,
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
+ }