phasegate 0.191.0 → 0.212.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 (67) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.ja.md +52 -15
  3. package/README.md +39 -11
  4. package/docs/ADR/030-injection-threat-model-and-trust-root.md +145 -0
  5. package/docs/guide/hooks-integration.md +50 -1
  6. package/docs/guide/installation.md +1 -1
  7. package/docs/guide/quick-vs-full-mode.md +1 -1
  8. package/docs/guide/skills-overview.md +7 -8
  9. package/package.json +1 -1
  10. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +131 -63
  11. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +31 -4
  12. package/scripts/harness/agent-integration/presentation/spotlight.ts +65 -0
  13. package/scripts/harness/ci-governance/application/dto/pin-integrity-input.ts +8 -0
  14. package/scripts/harness/ci-governance/application/dto/pin-integrity-output.ts +9 -0
  15. package/scripts/harness/ci-governance/application/dto/verify-integrity-input.ts +7 -0
  16. package/scripts/harness/ci-governance/application/dto/verify-integrity-output.ts +10 -0
  17. package/scripts/harness/ci-governance/application/usecases/pin-integrity-usecase.ts +60 -0
  18. package/scripts/harness/ci-governance/application/usecases/verify-integrity-usecase.ts +46 -0
  19. package/scripts/harness/ci-governance/composition-root.ts +67 -64
  20. package/scripts/harness/ci-governance/domain/ports/integrity-manifest-repository-port.ts +14 -0
  21. package/scripts/harness/ci-governance/domain/ports/sha256-hasher-port.ts +10 -0
  22. package/scripts/harness/ci-governance/domain/services/integrity-checker.ts +42 -0
  23. package/scripts/harness/ci-governance/domain/value-objects/integrity-drift.ts +16 -0
  24. package/scripts/harness/ci-governance/domain/value-objects/integrity-manifest.ts +49 -0
  25. package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +40 -0
  26. package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha256-hasher-adapter.ts +17 -0
  27. package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +7 -73
  28. package/scripts/harness/ci-governance/infrastructure/adapters/integrity-manifest-json-repository-adapter.ts +83 -0
  29. package/scripts/harness/ci-governance/presentation/handlers/integrity-handler.ts +76 -0
  30. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +53 -28
  31. package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +33 -8
  32. package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +90 -0
  33. package/scripts/harness/installation/application/bundled-skill-selection.ts +2 -5
  34. package/scripts/harness/main.ts +257 -105
  35. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +65 -3
  36. package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
  37. package/scripts/harness/setup/skill-deployer.ts +2 -4
  38. package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +82 -49
  39. package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +87 -58
  40. package/scripts/harness/validator-system/composition-root.ts +141 -99
  41. package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -0
  42. package/scripts/harness/validator-system/domain/ports/injection-scan-policy-port.ts +14 -0
  43. package/scripts/harness/validator-system/domain/services/coverage-attestation-gating-service.ts +56 -0
  44. package/scripts/harness/validator-system/domain/services/injection-pattern-scan-service.ts +118 -0
  45. package/scripts/harness/validator-system/domain/value-objects/coverage-gating-report.ts +67 -0
  46. package/scripts/harness/validator-system/domain/value-objects/injection-scan-report.ts +55 -0
  47. package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +35 -31
  48. package/scripts/harness/validator-system/infrastructure/adapters/adr-foundation-reference-adapter.ts +13 -7
  49. package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-gating-adapter.ts +87 -0
  50. package/scripts/harness/validator-system/infrastructure/adapters/file-system-injection-scan-adapter.ts +82 -0
  51. package/skills/README.md +1 -1
  52. package/skills/codebase-mapper/SKILL.md +1 -1
  53. package/skills/consistency-checker/references//343/203/201/343/202/247/343/203/203/343/202/257/343/203/252/343/202/271/343/203/210.md +1 -1
  54. package/skills/doc-health-checker/SKILL.md +148 -0
  55. package/skills/release-publisher/SKILL.md +101 -0
  56. package/skills/skill-creator/SKILL.md +74 -332
  57. package/skills/story-implementor/SKILL.md +52 -0
  58. package/skills/story-mapper/SKILL.md +4 -0
  59. package/skills/story-writer/SKILL.md +9 -0
  60. package/skills/uiux-designer/references/uiux-design-template.md +4 -4
  61. package/skills/unit-designer/SKILL.md +3 -1
  62. package/skills/doc-freshness-checker/SKILL.md +0 -140
  63. package/skills/implementation-planner/SKILL.md +0 -169
  64. package/skills/implementation-planner/references/document-structure.md +0 -116
  65. package/skills/implementation-planner/references/plan-template.md +0 -177
  66. package/skills/implementation-planner/references/workflow.md +0 -164
  67. package/skills/pointer-validator/SKILL.md +0 -105
@@ -15,6 +15,12 @@ const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
15
15
  const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
16
16
  const AFFECTS_INLINE_PATTERN = /^affects:\s*\[([^\]]*)\]\s*$/m;
17
17
  const LEGACY_ID_PATTERN = /^legacy_id:\s*([A-Z][\w]+-\d+)\s*$/m;
18
+ /** コミット本文の `Work-Item: WI-NNN` trailer 行から WI を抽出する */
19
+ const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*(WI-\d+)\s*$/gm;
20
+ /** ソースファイル先頭の `@work-item-id` / `@story` タグ配下の WI を抽出する */
21
+ const SOURCE_TAG_PATTERN = /@(?:work-item-id|story)\b[^\n\r]*/g;
22
+ /** harness ソースパスから unit / layer を切り出す(帰属フィルタ適用対象の判定) */
23
+ const HARNESS_SOURCE_PATH_PATTERN = /^scripts\/harness\/[^/]+\/[^/]+\//;
18
24
  const execFileAsync = promisify(execFile);
19
25
 
20
26
  export interface FileSystemStoryReflectionAdapterDeps {
@@ -167,13 +173,13 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
167
173
  { cwd: this.rootDir, maxBuffer: 32 * 1024 * 1024 },
168
174
  );
169
175
 
170
- return this.extractChangedPathsForStory(stdout, storyId);
176
+ return await this.extractChangedPathsForStory(stdout, storyId);
171
177
  } catch {
172
178
  return new Set<string>();
173
179
  }
174
180
  }
175
181
 
176
- private extractChangedPathsForStory(output: string, storyId: string): ReadonlySet<string> {
182
+ private async extractChangedPathsForStory(output: string, storyId: string): Promise<ReadonlySet<string>> {
177
183
  const changedPaths = new Set<string>();
178
184
  const trailerPattern = new RegExp(`Work-Item:[^\\n]*\\b${storyId}\\b`);
179
185
 
@@ -186,10 +192,16 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
186
192
  const body = commitBlock.slice(0, bodyEnd);
187
193
  if (!trailerPattern.test(body)) continue;
188
194
 
195
+ // 複数 Work-Item trailer を同梱するバッチコミットでは、changed paths が
196
+ // 全 trailer WI に一律帰属してしまう(over-attribution)。ソースパスに限り
197
+ // ファイルの帰属タグで storyId への帰属を絞り込む(WI-251)。
198
+ const isMultiWorkItemCommit = this.countWorkItemTrailers(body) >= 2;
189
199
  const nameOnly = commitBlock.slice(bodyEnd + 1);
190
200
  for (const changedPath of nameOnly.split(/\r?\n/)) {
191
201
  const trimmedPath = changedPath.trim();
192
- if (trimmedPath.length > 0 && trimmedPath !== "@@COMMIT@@") {
202
+ if (trimmedPath.length === 0 || trimmedPath === "@@COMMIT@@") continue;
203
+
204
+ if (await this.pathAttributesToStory(trimmedPath, storyId, isMultiWorkItemCommit)) {
193
205
  changedPaths.add(trimmedPath);
194
206
  }
195
207
  }
@@ -198,6 +210,56 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
198
210
  return changedPaths;
199
211
  }
200
212
 
213
+ private countWorkItemTrailers(body: string): number {
214
+ return body.match(WORK_ITEM_TRAILER_PATTERN)?.length ?? 0;
215
+ }
216
+
217
+ /**
218
+ * バッチコミットの changed path を storyId に帰属させてよいか判定する。
219
+ * - 単一 WI trailer コミット → 常に帰属(現行挙動を維持)
220
+ * - 複数 WI trailer × 非ソースパス → 帰属(絞り込み対象外)
221
+ * - 複数 WI trailer × ソースパス → ファイル内容(HEAD)のタグに storyId を含むときのみ帰属。
222
+ * タグ無し / ファイルが読めない → fail-closed で帰属維持(反映要求を緩めない)。
223
+ */
224
+ private async pathAttributesToStory(
225
+ changedPath: string,
226
+ storyId: string,
227
+ isMultiWorkItemCommit: boolean,
228
+ ): Promise<boolean> {
229
+ if (!isMultiWorkItemCommit) {
230
+ return true;
231
+ }
232
+
233
+ if (!HARNESS_SOURCE_PATH_PATTERN.test(changedPath)) {
234
+ return true;
235
+ }
236
+
237
+ const tags = await this.readSourceWorkItemTags(changedPath);
238
+ if (tags === null || tags.size === 0) {
239
+ return true;
240
+ }
241
+
242
+ return tags.has(storyId);
243
+ }
244
+
245
+ private async readSourceWorkItemTags(relativePath: string): Promise<Set<string> | null> {
246
+ let content: string;
247
+ try {
248
+ content = await readFile(path.join(this.rootDir, relativePath), "utf8");
249
+ } catch {
250
+ return null;
251
+ }
252
+
253
+ const tags = new Set<string>();
254
+ for (const line of content.match(SOURCE_TAG_PATTERN) ?? []) {
255
+ for (const id of line.match(/WI-\d+/g) ?? []) {
256
+ tags.add(id);
257
+ }
258
+ }
259
+
260
+ return tags;
261
+ }
262
+
201
263
  async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
202
264
  const absolutePath = path.join(this.rootDir, productPath);
203
265
 
@@ -6,12 +6,12 @@
6
6
  * ChangedFile[]をChangeClassificationに変換し、3拒否ルールを評価してQuickModeEligibilityを返すドメインサービス
7
7
  */
8
8
 
9
- import { ChangeCategory } from '../value-objects/change-category.js';
10
- import { ChangeClassification } from '../value-objects/change-classification.js';
11
- import { QuickModeEligibility } from '../value-objects/quick-mode-eligibility.js';
12
- import { isCommentOnlyDiff } from './comment-only-diff-detector.js';
13
- import type { ChangedFile } from '../value-objects/changed-file.js';
14
- import type { QuickModeConfig } from '../value-objects/quick-mode-config.js';
9
+ import { ChangeCategory } from "../value-objects/change-category.js";
10
+ import { ChangeClassification } from "../value-objects/change-classification.js";
11
+ import type { ChangedFile } from "../value-objects/changed-file.js";
12
+ import type { QuickModeConfig } from "../value-objects/quick-mode-config.js";
13
+ import { QuickModeEligibility } from "../value-objects/quick-mode-eligibility.js";
14
+ import { isCommentOnlyDiff } from "./comment-only-diff-detector.js";
15
15
 
16
16
  // リスク順優先度(api > domain > feature > bugfix > test > config > docs)
17
17
  const RISK_PRIORITY: Record<string, number> = {
@@ -30,48 +30,54 @@ function categorizeFile(file: ChangedFile): ChangeCategory {
30
30
  // Config files must stay config even when Edit payload snippets look like
31
31
  // comment/whitespace-only diffs. Config recovery guidance depends on this.
32
32
  if (
33
- filePath.endsWith('.config.json') ||
34
- filePath.endsWith('.config.ts') ||
35
- filePath.endsWith('phasegate.config.json')
33
+ filePath.endsWith(".config.json") ||
34
+ filePath.endsWith(".config.ts") ||
35
+ filePath.endsWith("phasegate.config.json")
36
36
  ) {
37
- return ChangeCategory.fromString('config');
37
+ return ChangeCategory.fromString("config");
38
38
  }
39
39
 
40
40
  if (isCommentOnlyDiff(file)) {
41
- return ChangeCategory.fromString('docs');
41
+ return ChangeCategory.fromString("docs");
42
42
  }
43
43
 
44
44
  // api: *port.ts or *adapter.ts(最高優先度)
45
- if (filePath.endsWith('port.ts') || filePath.endsWith('adapter.ts')) {
46
- return ChangeCategory.fromString('api');
45
+ if (filePath.endsWith("port.ts") || filePath.endsWith("adapter.ts")) {
46
+ return ChangeCategory.fromString("api");
47
47
  }
48
48
 
49
49
  // test: __tests__/ 配下 or *.test.ts or *.spec.ts(domainより優先)
50
- if (
51
- filePath.includes('__tests__/') ||
52
- filePath.endsWith('.test.ts') ||
53
- filePath.endsWith('.spec.ts')
54
- ) {
55
- return ChangeCategory.fromString('test');
50
+ if (filePath.includes("__tests__/") || filePath.endsWith(".test.ts") || filePath.endsWith(".spec.ts")) {
51
+ return ChangeCategory.fromString("test");
56
52
  }
57
53
 
58
54
  // domain: domain/ 配下
59
- if (filePath.includes('/domain/') || filePath.startsWith('domain/')) {
60
- return ChangeCategory.fromString('domain');
55
+ if (filePath.includes("/domain/") || filePath.startsWith("domain/")) {
56
+ return ChangeCategory.fromString("domain");
61
57
  }
62
58
 
63
59
  // docs: docs/ 配下
64
- if (filePath.startsWith('docs/') || filePath.includes('/docs/')) {
65
- return ChangeCategory.fromString('docs');
60
+ if (filePath.startsWith("docs/") || filePath.includes("/docs/")) {
61
+ return ChangeCategory.fromString("docs");
62
+ }
63
+
64
+ // docs: skills/ 配下の .md(指示文書。SKILL.md / references/*.md 等)は
65
+ // ソースコードのフェーズゲート対象外とし docs に分類する。品質・完全性は
66
+ // 専用防御(skill-quality corpus 適合テスト・advisory allowlist pin・
67
+ // integrity pin(WI-254)・L3-006 injection scanner(WI-259))が担う。
68
+ // skills/ 配下の非 .md は下の feature/bugfix フォールバックに落ちて fail-closed を維持する。
69
+ // @work-item-id WI-261
70
+ if (filePath.startsWith("skills/") && filePath.endsWith(".md")) {
71
+ return ChangeCategory.fromString("docs");
66
72
  }
67
73
 
68
74
  // feature: domain/ 以外の CREATE
69
- if (changeKind === 'CREATE') {
70
- return ChangeCategory.fromString('feature');
75
+ if (changeKind === "CREATE") {
76
+ return ChangeCategory.fromString("feature");
71
77
  }
72
78
 
73
79
  // bugfix: domain/ 以外の MODIFY/DELETE
74
- return ChangeCategory.fromString('bugfix');
80
+ return ChangeCategory.fromString("bugfix");
75
81
  }
76
82
 
77
83
  export class QuickModeJudgmentEngine {
@@ -107,7 +113,7 @@ export class QuickModeJudgmentEngine {
107
113
  const classification = this.classify(changedFiles, config);
108
114
 
109
115
  // 1. MIXED_CHANGES評価: allowedCategories 外のカテゴリが含まれる場合
110
- if (config.isFullModeRequiredFor('mixedCategories')) {
116
+ if (config.isFullModeRequiredFor("mixedCategories")) {
111
117
  const notAllowedFiles: ChangedFile[] = [];
112
118
  classification.categorizedFiles.forEach((files, categoryKey) => {
113
119
  if (!config.isAllowed(categoryKey)) {
@@ -117,45 +123,43 @@ export class QuickModeJudgmentEngine {
117
123
 
118
124
  if (notAllowedFiles.length > 0) {
119
125
  return QuickModeEligibility.rejected(
120
- 'MIXED_CHANGES',
126
+ "MIXED_CHANGES",
121
127
  notAllowedFiles,
122
- `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(', ')}`
128
+ `allowedCategories外のファイルが含まれています: ${notAllowedFiles.map((f) => f.filePath).join(", ")}`,
123
129
  );
124
130
  }
125
131
  }
126
132
 
127
133
  // 2. NEW_DOMAIN評価: domain/ 配下かつ changeKind=CREATE
128
- if (config.isFullModeRequiredFor('newDomainFile')) {
134
+ if (config.isFullModeRequiredFor("newDomainFile")) {
129
135
  const newDomainFiles = changedFiles.filter(
130
- (f) =>
131
- (f.filePath.includes('/domain/') || f.filePath.startsWith('domain/')) &&
132
- f.changeKind === 'CREATE'
136
+ (f) => (f.filePath.includes("/domain/") || f.filePath.startsWith("domain/")) && f.changeKind === "CREATE",
133
137
  );
134
138
 
135
139
  if (newDomainFiles.length > 0) {
136
140
  return QuickModeEligibility.rejected(
137
- 'NEW_DOMAIN',
141
+ "NEW_DOMAIN",
138
142
  newDomainFiles,
139
- `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(', ')}`
143
+ `domain/配下に新規ファイルが追加されています: ${newDomainFiles.map((f) => f.filePath).join(", ")}`,
140
144
  );
141
145
  }
142
146
  }
143
147
 
144
148
  // 3. API_CONTRACT評価: *port.ts / *adapter.ts の変更
145
- if (config.isFullModeRequiredFor('apiContractChange')) {
149
+ if (config.isFullModeRequiredFor("apiContractChange")) {
146
150
  const apiContractFiles = changedFiles.filter(
147
- (f) => (f.filePath.endsWith('port.ts') || f.filePath.endsWith('adapter.ts')) && !isCommentOnlyDiff(f)
151
+ (f) => (f.filePath.endsWith("port.ts") || f.filePath.endsWith("adapter.ts")) && !isCommentOnlyDiff(f),
148
152
  );
149
153
 
150
154
  if (apiContractFiles.length > 0) {
151
155
  return QuickModeEligibility.rejected(
152
- 'API_CONTRACT',
156
+ "API_CONTRACT",
153
157
  apiContractFiles,
154
- `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(', ')}`
158
+ `Port/Adapterインターフェースファイルの変更が含まれています: ${apiContractFiles.map((f) => f.filePath).join(", ")}`,
155
159
  );
156
160
  }
157
161
  }
158
162
 
159
- return QuickModeEligibility.eligible('すべてのファイルが許可カテゴリ内です');
163
+ return QuickModeEligibility.eligible("すべてのファイルが許可カテゴリ内です");
160
164
  }
161
165
  }
@@ -33,16 +33,14 @@ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
33
33
  "cascade-updater",
34
34
  "codebase-mapper",
35
35
  "consistency-checker",
36
- "doc-freshness-checker",
36
+ "doc-health-checker",
37
37
  "engineering-perspective",
38
38
  "implementation-readiness-checker",
39
- "pointer-validator",
40
39
  "test-coverage-checker",
41
40
  ],
42
41
  aidlc: [
43
42
  "domain-designer",
44
43
  "environment-designer",
45
- "implementation-planner",
46
44
  "it-test-designer",
47
45
  "it-test-logic-designer",
48
46
  "logical-designer",
@@ -60,7 +58,7 @@ export const SKILL_CATEGORIES: Record<SkillCategory, readonly string[]> = {
60
58
  "unit-test-logic-designer",
61
59
  ],
62
60
  utility: ["codex-delegator", "skill-creator"],
63
- guidance: ["phasegate-toolkit-guide", "phasegate-config-doctor"],
61
+ guidance: ["phasegate-toolkit-guide", "phasegate-config-doctor", "release-publisher"],
64
62
  } as const;
65
63
 
66
64
  export function getSkillsForSet(skillSet: SkillSet): string[] {
@@ -5,24 +5,29 @@
5
5
  * RunL2ValidatorsUseCase — H08-01: L2バリデータ実行
6
6
  * @work-item-id WI-110 / WI-111 / WI-140 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138
7
7
  */
8
- import { readFile } from 'node:fs/promises';
9
- import { ValidatorId, InvalidValidatorIdError } from '../../domain/value-objects/validator-id.js';
10
- import { ValidationResult, type HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
11
- import type { ValidatorRegistry } from '../../domain/services/validator-registry.js';
12
- import { type ValidatorExecutionService, ValidatorExecutionError } from '../../domain/services/validator-execution-service.js';
13
- import type { ValidationResultContractMapper } from '../mappers/validation-result-contract-mapper.js';
14
- import type { ValidationResultContract } from '../dto/validation-result-contract.js';
15
- import type { RunL2ValidatorsInput } from '../dto/run-l2-validators-input.js';
16
- import type { ValidatorConfigPort } from '../../domain/ports/validator-config-port.js';
17
- import type { PhaseGatePolicyPort } from '../../domain/ports/phase-gate-policy-port.js';
18
- import type { MetadataPolicyPort } from '../../domain/ports/metadata-policy-port.js';
19
- import type { TestQualityAnalyzerPort } from '../../domain/ports/test-quality-analyzer-port.js';
20
- import type { CliCommandRegistryPort } from '../../domain/ports/cli-command-registry-port.js';
21
- import type { E2eTestFileRegistryPort } from '../../domain/ports/e2e-test-file-registry-port.js';
22
- import { CliE2eTestExistenceService } from '../../domain/services/cli-e2e-test-existence-service.js';
23
- import type { WorkItemStatusPolicyPort } from '../../domain/ports/work-item-status-policy-port.js';
24
- import type { ContractTraceabilityPolicyPort } from '../../domain/ports/contract-traceability-policy-port.js';
25
- import { ContractTraceabilityCoverageService } from '../../domain/services/contract-traceability-coverage-service.js';
8
+ import { readFile } from "node:fs/promises";
9
+ import type { CliCommandRegistryPort } from "../../domain/ports/cli-command-registry-port.js";
10
+ import type { ContractTraceabilityPolicyPort } from "../../domain/ports/contract-traceability-policy-port.js";
11
+ import type { CoverageAttestationGatingPolicyPort } from "../../domain/ports/coverage-attestation-gating-policy-port.js";
12
+ import type { E2eTestFileRegistryPort } from "../../domain/ports/e2e-test-file-registry-port.js";
13
+ import type { MetadataPolicyPort } from "../../domain/ports/metadata-policy-port.js";
14
+ import type { PhaseGatePolicyPort } from "../../domain/ports/phase-gate-policy-port.js";
15
+ import type { TestQualityAnalyzerPort } from "../../domain/ports/test-quality-analyzer-port.js";
16
+ import type { ValidatorConfigPort } from "../../domain/ports/validator-config-port.js";
17
+ import type { WorkItemStatusPolicyPort } from "../../domain/ports/work-item-status-policy-port.js";
18
+ import { CliE2eTestExistenceService } from "../../domain/services/cli-e2e-test-existence-service.js";
19
+ import { ContractTraceabilityCoverageService } from "../../domain/services/contract-traceability-coverage-service.js";
20
+ import { CoverageAttestationGatingService } from "../../domain/services/coverage-attestation-gating-service.js";
21
+ import {
22
+ ValidatorExecutionError,
23
+ type ValidatorExecutionService,
24
+ } from "../../domain/services/validator-execution-service.js";
25
+ import type { ValidatorRegistry } from "../../domain/services/validator-registry.js";
26
+ import { type HarnessErrorLike, ValidationResult } from "../../domain/value-objects/validation-result.js";
27
+ import { ValidatorId } from "../../domain/value-objects/validator-id.js";
28
+ import type { RunL2ValidatorsInput } from "../dto/run-l2-validators-input.js";
29
+ import type { ValidationResultContract } from "../dto/validation-result-contract.js";
30
+ import type { ValidationResultContractMapper } from "../mappers/validation-result-contract-mapper.js";
26
31
 
27
32
  export interface RunL2ValidatorsUseCaseDeps {
28
33
  validatorRegistry: ValidatorRegistry;
@@ -36,6 +41,7 @@ export interface RunL2ValidatorsUseCaseDeps {
36
41
  e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
37
42
  workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
38
43
  contractTraceabilityPolicyPort?: ContractTraceabilityPolicyPort;
44
+ coverageAttestationGatingPolicyPort?: CoverageAttestationGatingPolicyPort;
39
45
  }
40
46
 
41
47
  export class RunL2ValidatorsUseCase {
@@ -50,8 +56,10 @@ export class RunL2ValidatorsUseCase {
50
56
  private readonly e2eTestFileRegistryPort?: E2eTestFileRegistryPort;
51
57
  private readonly workItemStatusPolicyPort?: WorkItemStatusPolicyPort;
52
58
  private readonly contractTraceabilityPolicyPort?: ContractTraceabilityPolicyPort;
59
+ private readonly coverageAttestationGatingPolicyPort?: CoverageAttestationGatingPolicyPort;
53
60
  private readonly cliE2eTestExistenceService = new CliE2eTestExistenceService();
54
61
  private readonly contractTraceabilityCoverageService = new ContractTraceabilityCoverageService();
62
+ private readonly coverageAttestationGatingService = new CoverageAttestationGatingService();
55
63
 
56
64
  constructor(deps: RunL2ValidatorsUseCaseDeps) {
57
65
  this.registry = deps.validatorRegistry;
@@ -65,6 +73,7 @@ export class RunL2ValidatorsUseCase {
65
73
  this.e2eTestFileRegistryPort = deps.e2eTestFileRegistryPort;
66
74
  this.workItemStatusPolicyPort = deps.workItemStatusPolicyPort;
67
75
  this.contractTraceabilityPolicyPort = deps.contractTraceabilityPolicyPort;
76
+ this.coverageAttestationGatingPolicyPort = deps.coverageAttestationGatingPolicyPort;
68
77
  }
69
78
 
70
79
  async execute(input: RunL2ValidatorsInput): Promise<readonly ValidationResultContract[]> {
@@ -73,24 +82,27 @@ export class RunL2ValidatorsUseCase {
73
82
  if (input.validatorIds && input.validatorIds.length > 0) {
74
83
  validatorIds = input.validatorIds.map((id) => ValidatorId.create(id));
75
84
  } else {
76
- validatorIds = this.registry.listByLayer('L2').map((d) => d.validatorId);
85
+ validatorIds = this.registry.listByLayer("L2").map((d) => d.validatorId);
77
86
  }
78
87
 
79
88
  const definitions = this.registry.select(validatorIds);
80
89
 
81
90
  // LayerConfig取得
82
- let layerConfig;
91
+ let layerConfig: Awaited<ReturnType<ValidatorConfigPort["getLayerConfig"]>>;
83
92
  try {
84
- layerConfig = await this.configPort.getLayerConfig('L2');
93
+ layerConfig = await this.configPort.getLayerConfig("L2");
85
94
  } catch (err) {
86
- throw new ValidatorExecutionError(`Failed to get L2 LayerConfig: ${err instanceof Error ? err.message : String(err)}`, err);
95
+ throw new ValidatorExecutionError(
96
+ `Failed to get L2 LayerConfig: ${err instanceof Error ? err.message : String(err)}`,
97
+ err,
98
+ );
87
99
  }
88
100
 
89
101
  const results = this.executionService.execute(definitions, [layerConfig]);
90
102
  const overrideMap = new Map<string, ValidationResult>(results.map((result) => [result.validatorId.value, result]));
91
103
 
92
104
  if (this.phaseGatePolicyPort) {
93
- const l2001Result = overrideMap.get('L2-001');
105
+ const l2001Result = overrideMap.get("L2-001");
94
106
  if (l2001Result && !l2001Result.skipped) {
95
107
  const policyResult = await this.phaseGatePolicyPort.checkPrerequisites({
96
108
  unitName: input.unitName,
@@ -98,24 +110,20 @@ export class RunL2ValidatorsUseCase {
98
110
  });
99
111
  if (!policyResult.satisfied) {
100
112
  overrideMap.set(
101
- 'L2-001',
102
- ValidationResult.fail(
103
- ValidatorId.create('L2-001'),
104
- [...policyResult.violations],
105
- 0,
106
- ),
113
+ "L2-001",
114
+ ValidationResult.fail(ValidatorId.create("L2-001"), [...policyResult.violations], 0),
107
115
  );
108
116
  }
109
117
  }
110
118
  }
111
119
 
112
120
  if (this.metadataPolicyPort) {
113
- const l2002Result = overrideMap.get('L2-002');
121
+ const l2002Result = overrideMap.get("L2-002");
114
122
  if (l2002Result && !l2002Result.skipped) {
115
123
  const allErrors: HarnessErrorLike[] = [];
116
124
  for (const filePath of input.targetPaths) {
117
125
  try {
118
- const fileContent = await readFile(filePath, 'utf-8');
126
+ const fileContent = await readFile(filePath, "utf-8");
119
127
  const policyResult = await this.metadataPolicyPort.validateMetadata({ filePath, fileContent });
120
128
  if (!policyResult.passed) {
121
129
  allErrors.push(...policyResult.errors);
@@ -125,52 +133,52 @@ export class RunL2ValidatorsUseCase {
125
133
  }
126
134
  }
127
135
  if (allErrors.length > 0) {
128
- overrideMap.set('L2-002', ValidationResult.fail(ValidatorId.create('L2-002'), allErrors, 0));
136
+ overrideMap.set("L2-002", ValidationResult.fail(ValidatorId.create("L2-002"), allErrors, 0));
129
137
  }
130
138
  }
131
139
  }
132
140
 
133
141
  if (this.testQualityAnalyzerPort) {
134
- const l2003Result = overrideMap.get('L2-003');
142
+ const l2003Result = overrideMap.get("L2-003");
135
143
  if (l2003Result && !l2003Result.skipped) {
136
144
  const analysisResult = await this.testQualityAnalyzerPort.analyzeTestFiles(input.targetPaths);
137
145
  const allViolations: HarnessErrorLike[] = analysisResult.results
138
146
  .filter((r) => !r.passed)
139
147
  .flatMap((r) => [...r.violations]);
140
148
  if (allViolations.length > 0) {
141
- overrideMap.set('L2-003', ValidationResult.fail(ValidatorId.create('L2-003'), allViolations, 0));
149
+ overrideMap.set("L2-003", ValidationResult.fail(ValidatorId.create("L2-003"), allViolations, 0));
142
150
  }
143
151
  }
144
152
  }
145
153
 
146
154
  if (this.cliCommandRegistryPort && this.e2eTestFileRegistryPort) {
147
- const l2013Result = overrideMap.get('L2-013');
155
+ const l2013Result = overrideMap.get("L2-013");
148
156
  if (l2013Result && !l2013Result.skipped) {
149
157
  const commands = await this.cliCommandRegistryPort.getRegisteredCommands();
150
158
  const e2eFiles = await this.e2eTestFileRegistryPort.getE2eTestFiles();
151
159
  const report = this.cliE2eTestExistenceService.check(commands, e2eFiles);
152
160
  if (report.hasViolations()) {
153
161
  const errors = report.toMessages().map((msg) => ({
154
- code: { value: 'L2-013', toString: () => 'L2-013' },
155
- severity: { value: 'error', toString: () => 'error' },
162
+ code: { value: "L2-013", toString: () => "L2-013" },
163
+ severity: { value: "error", toString: () => "error" },
156
164
  message: msg,
157
- suggestion: 'Add an E2E test for each registered CLI command in cli-harness.test.ts.',
165
+ suggestion: "Add an E2E test for each registered CLI command in cli-harness.test.ts.",
158
166
  }));
159
- overrideMap.set('L2-013', ValidationResult.fail(ValidatorId.create('L2-013'), errors, 0));
167
+ overrideMap.set("L2-013", ValidationResult.fail(ValidatorId.create("L2-013"), errors, 0));
160
168
  } else {
161
- overrideMap.set('L2-013', ValidationResult.pass(ValidatorId.create('L2-013'), 0));
169
+ overrideMap.set("L2-013", ValidationResult.pass(ValidatorId.create("L2-013"), 0));
162
170
  }
163
171
  }
164
172
  }
165
173
 
166
174
  if (this.workItemStatusPolicyPort) {
167
- const l2014Result = overrideMap.get('L2-014');
175
+ const l2014Result = overrideMap.get("L2-014");
168
176
  if (l2014Result && !l2014Result.skipped) {
169
177
  const staleReports = await this.workItemStatusPolicyPort.findStaleReports(input.targetPaths);
170
178
  if (staleReports.length > 0) {
171
179
  const errors = staleReports.map((report) => ({
172
- code: { value: 'L2-014', toString: () => 'L2-014' },
173
- severity: { value: 'error', toString: () => 'error' },
180
+ code: { value: "L2-014", toString: () => "L2-014" },
181
+ severity: { value: "error", toString: () => "error" },
174
182
  message: `${report.id} status is stale: current=${report.currentStatus}, derived=${report.derivedStatus}`,
175
183
  suggestion: report.nextAction,
176
184
  workItemId: report.id,
@@ -179,21 +187,21 @@ export class RunL2ValidatorsUseCase {
179
187
  derivedStatus: report.derivedStatus,
180
188
  evidence: report.evidence,
181
189
  }));
182
- overrideMap.set('L2-014', ValidationResult.fail(ValidatorId.create('L2-014'), errors, 0));
190
+ overrideMap.set("L2-014", ValidationResult.fail(ValidatorId.create("L2-014"), errors, 0));
183
191
  } else {
184
- overrideMap.set('L2-014', ValidationResult.pass(ValidatorId.create('L2-014'), 0));
192
+ overrideMap.set("L2-014", ValidationResult.pass(ValidatorId.create("L2-014"), 0));
185
193
  }
186
194
  }
187
195
  }
188
196
 
189
197
  if (this.contractTraceabilityPolicyPort) {
190
- const l2015Result = overrideMap.get('L2-015');
198
+ const l2015Result = overrideMap.get("L2-015");
191
199
  if (l2015Result && !l2015Result.skipped) {
192
200
  const inputModel = await this.contractTraceabilityPolicyPort.collect(input.targetPaths);
193
201
  const report = this.contractTraceabilityCoverageService.check(inputModel);
194
202
  if (report.hasFindings()) {
195
203
  const errors = report.findings.map((finding) => ({
196
- code: { value: 'L2-015', toString: () => 'L2-015' },
204
+ code: { value: "L2-015", toString: () => "L2-015" },
197
205
  severity: { value: finding.severity, toString: () => finding.severity },
198
206
  message: finding.message,
199
207
  suggestion: finding.suggestion,
@@ -201,9 +209,34 @@ export class RunL2ValidatorsUseCase {
201
209
  subject: finding.subject,
202
210
  sourcePath: finding.sourcePath,
203
211
  }));
204
- overrideMap.set('L2-015', ValidationResult.fail(ValidatorId.create('L2-015'), errors, 0));
212
+ overrideMap.set("L2-015", ValidationResult.fail(ValidatorId.create("L2-015"), errors, 0));
205
213
  } else {
206
- overrideMap.set('L2-015', ValidationResult.pass(ValidatorId.create('L2-015'), 0));
214
+ overrideMap.set("L2-015", ValidationResult.pass(ValidatorId.create("L2-015"), 0));
215
+ }
216
+ }
217
+ }
218
+
219
+ // WI-258 / ADR-030 §Decision.3.②: L2-016 coverage-attestation-gating。
220
+ // bare ✅(attestation 参照なし)は error violation(fail-closed)。ungated-legacy マーカーは
221
+ // 免除だが warning(見える負債)で件数報告する。warning-only の fail は ADR-017 の集約規則で
222
+ // failOnWarning=false のとき overall PASS 扱いとなる。
223
+ if (this.coverageAttestationGatingPolicyPort) {
224
+ const l2016Result = overrideMap.get("L2-016");
225
+ if (l2016Result && !l2016Result.skipped) {
226
+ const models = await this.coverageAttestationGatingPolicyPort.collect();
227
+ const report = this.coverageAttestationGatingService.check(models);
228
+ const findings = [...report.violations, ...report.warnings];
229
+ if (findings.length > 0) {
230
+ const errors: HarnessErrorLike[] = findings.map((finding) => ({
231
+ code: { value: "L2-016", toString: () => "L2-016" },
232
+ severity: { value: finding.severity, toString: () => finding.severity },
233
+ message: finding.message,
234
+ suggestion: finding.suggestion,
235
+ sourcePath: finding.sourcePath,
236
+ }));
237
+ overrideMap.set("L2-016", ValidationResult.fail(ValidatorId.create("L2-016"), errors, 0));
238
+ } else {
239
+ overrideMap.set("L2-016", ValidationResult.pass(ValidatorId.create("L2-016"), 0));
207
240
  }
208
241
  }
209
242
  }