phasegate 0.191.0 → 0.222.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 +15 -0
- package/README.ja.md +52 -15
- package/README.md +39 -11
- package/docs/ADR/030-injection-threat-model-and-trust-root.md +145 -0
- package/docs/guide/hooks-integration.md +101 -1
- package/docs/guide/installation.md +1 -1
- package/docs/guide/quick-vs-full-mode.md +1 -1
- package/docs/guide/skills-overview.md +7 -8
- package/package.json +1 -1
- package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +131 -63
- package/scripts/harness/agent-integration/presentation/session-start-hook.ts +31 -4
- package/scripts/harness/agent-integration/presentation/spotlight.ts +65 -0
- package/scripts/harness/biome-ast-engine/application/mappers/build-harness-error-payload-output-mapper.ts +3 -3
- package/scripts/harness/biome-ast-engine/infrastructure/mappers/rule-violation-code-mapper.ts +3 -3
- package/scripts/harness/biome-ast-engine/presentation/cli/harness-lint-command-handler.ts +6 -4
- package/scripts/harness/ci-governance/application/dto/pin-integrity-input.ts +8 -0
- package/scripts/harness/ci-governance/application/dto/pin-integrity-output.ts +9 -0
- package/scripts/harness/ci-governance/application/dto/verify-integrity-input.ts +7 -0
- package/scripts/harness/ci-governance/application/dto/verify-integrity-output.ts +10 -0
- package/scripts/harness/ci-governance/application/usecases/pin-integrity-usecase.ts +60 -0
- package/scripts/harness/ci-governance/application/usecases/verify-integrity-usecase.ts +46 -0
- package/scripts/harness/ci-governance/composition-root.ts +67 -64
- package/scripts/harness/ci-governance/domain/ports/integrity-manifest-repository-port.ts +14 -0
- package/scripts/harness/ci-governance/domain/ports/sha256-hasher-port.ts +10 -0
- package/scripts/harness/ci-governance/domain/services/integrity-checker.ts +42 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-drift.ts +16 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-manifest.ts +49 -0
- package/scripts/harness/ci-governance/domain/value-objects/integrity-target.ts +40 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/file-system-sha256-hasher-adapter.ts +17 -0
- package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +7 -73
- package/scripts/harness/ci-governance/infrastructure/adapters/integrity-manifest-json-repository-adapter.ts +83 -0
- package/scripts/harness/ci-governance/presentation/handlers/integrity-handler.ts +76 -0
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +60 -28
- package/scripts/harness/harness-api/domain/value-objects/ci-check-result.ts +33 -8
- package/scripts/harness/harness-api/domain/value-objects/known-harness-commands.ts +90 -0
- package/scripts/harness/harness-error/infrastructure/registry/l1-error-definitions.ts +9 -9
- package/scripts/harness/installation/application/bundled-skill-selection.ts +2 -5
- package/scripts/harness/installation/application/usecases/run-reconcile.ts +69 -1
- package/scripts/harness/main.ts +257 -105
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +65 -3
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
- package/scripts/harness/setup/skill-deployer.ts +2 -4
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +82 -49
- package/scripts/harness/validator-system/application/use-cases/run-l3-validators-usecase.ts +133 -53
- package/scripts/harness/validator-system/composition-root.ts +153 -99
- package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -0
- package/scripts/harness/validator-system/domain/ports/coverage-attestation-verification-policy-port.ts +30 -0
- package/scripts/harness/validator-system/domain/ports/injection-scan-policy-port.ts +14 -0
- package/scripts/harness/validator-system/domain/services/coverage-attestation-gating-service.ts +56 -0
- package/scripts/harness/validator-system/domain/services/coverage-attestation-verification-service.ts +45 -0
- package/scripts/harness/validator-system/domain/services/injection-pattern-scan-service.ts +118 -0
- package/scripts/harness/validator-system/domain/value-objects/attestation-verification-report.ts +59 -0
- package/scripts/harness/validator-system/domain/value-objects/coverage-gating-report.ts +67 -0
- package/scripts/harness/validator-system/domain/value-objects/injection-scan-report.ts +55 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +37 -31
- package/scripts/harness/validator-system/infrastructure/adapters/adr-foundation-reference-adapter.ts +13 -7
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-gating-adapter.ts +87 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-coverage-attestation-verification-adapter.ts +165 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-injection-scan-adapter.ts +82 -0
- package/skills/README.md +1 -1
- package/skills/codebase-mapper/SKILL.md +1 -1
- 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
- package/skills/doc-health-checker/SKILL.md +148 -0
- package/skills/release-publisher/SKILL.md +101 -0
- package/skills/skill-creator/SKILL.md +74 -332
- package/skills/story-implementor/SKILL.md +52 -0
- package/skills/story-mapper/SKILL.md +4 -0
- package/skills/story-writer/SKILL.md +9 -0
- package/skills/uiux-designer/references/uiux-design-template.md +4 -4
- package/skills/unit-designer/SKILL.md +3 -1
- package/templates/.claude/scripts/deny-check.sh +260 -0
- package/skills/doc-freshness-checker/SKILL.md +0 -140
- package/skills/implementation-planner/SKILL.md +0 -169
- package/skills/implementation-planner/references/document-structure.md +0 -116
- package/skills/implementation-planner/references/plan-template.md +0 -177
- package/skills/implementation-planner/references/workflow.md +0 -164
- 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
|
|
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
|
|
10
|
-
import { ChangeClassification } from
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
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(
|
|
34
|
-
filePath.endsWith(
|
|
35
|
-
filePath.endsWith(
|
|
33
|
+
filePath.endsWith(".config.json") ||
|
|
34
|
+
filePath.endsWith(".config.ts") ||
|
|
35
|
+
filePath.endsWith("phasegate.config.json")
|
|
36
36
|
) {
|
|
37
|
-
return ChangeCategory.fromString(
|
|
37
|
+
return ChangeCategory.fromString("config");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (isCommentOnlyDiff(file)) {
|
|
41
|
-
return ChangeCategory.fromString(
|
|
41
|
+
return ChangeCategory.fromString("docs");
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// api: *port.ts or *adapter.ts(最高優先度)
|
|
45
|
-
if (filePath.endsWith(
|
|
46
|
-
return ChangeCategory.fromString(
|
|
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
|
-
|
|
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(
|
|
60
|
-
return ChangeCategory.fromString(
|
|
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(
|
|
65
|
-
return ChangeCategory.fromString(
|
|
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 ===
|
|
70
|
-
return ChangeCategory.fromString(
|
|
75
|
+
if (changeKind === "CREATE") {
|
|
76
|
+
return ChangeCategory.fromString("feature");
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// bugfix: domain/ 以外の MODIFY/DELETE
|
|
74
|
-
return ChangeCategory.fromString(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
149
|
+
if (config.isFullModeRequiredFor("apiContractChange")) {
|
|
146
150
|
const apiContractFiles = changedFiles.filter(
|
|
147
|
-
(f) => (f.filePath.endsWith(
|
|
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
|
-
|
|
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-
|
|
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
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
12
|
-
import
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
15
|
-
import type {
|
|
16
|
-
import type { ValidatorConfigPort } from
|
|
17
|
-
import type {
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
import {
|
|
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(
|
|
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(
|
|
93
|
+
layerConfig = await this.configPort.getLayerConfig("L2");
|
|
85
94
|
} catch (err) {
|
|
86
|
-
throw new ValidatorExecutionError(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
155
|
-
severity: { value:
|
|
162
|
+
code: { value: "L2-013", toString: () => "L2-013" },
|
|
163
|
+
severity: { value: "error", toString: () => "error" },
|
|
156
164
|
message: msg,
|
|
157
|
-
suggestion:
|
|
165
|
+
suggestion: "Add an E2E test for each registered CLI command in cli-harness.test.ts.",
|
|
158
166
|
}));
|
|
159
|
-
overrideMap.set(
|
|
167
|
+
overrideMap.set("L2-013", ValidationResult.fail(ValidatorId.create("L2-013"), errors, 0));
|
|
160
168
|
} else {
|
|
161
|
-
overrideMap.set(
|
|
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(
|
|
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:
|
|
173
|
-
severity: { value:
|
|
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(
|
|
190
|
+
overrideMap.set("L2-014", ValidationResult.fail(ValidatorId.create("L2-014"), errors, 0));
|
|
183
191
|
} else {
|
|
184
|
-
overrideMap.set(
|
|
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(
|
|
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:
|
|
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(
|
|
212
|
+
overrideMap.set("L2-015", ValidationResult.fail(ValidatorId.create("L2-015"), errors, 0));
|
|
205
213
|
} else {
|
|
206
|
-
overrideMap.set(
|
|
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
|
}
|