phasegate 0.183.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.
- package/CHANGELOG.md +20 -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 +50 -1
- package/docs/guide/installation.md +1 -1
- package/docs/guide/quick-vs-full-mode.md +1 -1
- package/docs/guide/skills-overview.md +17 -17
- 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/infrastructure/parsers/comment-density-parser.ts +51 -10
- 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 +68 -65
- 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/adr-foundation-existence-adapter.ts +26 -2
- 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 +8 -1
- 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 +53 -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/installation/application/bundled-skill-selection.ts +2 -5
- package/scripts/harness/main.ts +257 -105
- package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -0
- package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +8 -0
- package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +137 -2
- package/scripts/harness/quick-mode/domain/services/quick-mode-judgment-engine.ts +44 -40
- package/scripts/harness/setup/skill-deployer.ts +8 -10
- package/scripts/harness/skill-quality/domain/services/skill-structure-validator.ts +13 -2
- package/scripts/harness/skill-quality/domain/types/skill-kind.ts +6 -0
- package/scripts/harness/skill-quality/domain/value-objects/skill-structure.ts +24 -8
- 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 +87 -58
- package/scripts/harness/validator-system/composition-root.ts +141 -99
- package/scripts/harness/validator-system/domain/ports/coverage-attestation-gating-policy-port.ts +14 -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/injection-pattern-scan-service.ts +118 -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 +35 -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-injection-scan-adapter.ts +82 -0
- package/skills/README.md +1 -1
- package/skills/cascade-updater/SKILL.md +3 -3
- package/skills/codebase-mapper/SKILL.md +6 -6
- package/skills/codex-delegator/SKILL.md +4 -3
- package/skills/codex-delegator/references/prompt-patterns.md +3 -3
- package/skills/codex-delegator/references/review-dimensions.md +1 -1
- package/skills/consistency-checker/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/domain-designer/SKILL.md +4 -2
- package/skills/engineering-perspective/SKILL.md +1 -0
- package/skills/environment-designer/SKILL.md +8 -6
- package/skills/implementation-readiness-checker/SKILL.md +2 -1
- package/skills/it-test-designer/SKILL.md +10 -8
- package/skills/it-test-logic-designer/SKILL.md +11 -9
- package/skills/it-test-logic-designer/references/repository-test-patterns.md +8 -1
- package/skills/logical-designer/SKILL.md +5 -3
- package/skills/mock-designer/SKILL.md +10 -6
- package/skills/phasegate-config-doctor/SKILL.md +3 -2
- package/skills/phasegate-toolkit-guide/SKILL.md +1 -0
- package/skills/quick-implementor/SKILL.md +1 -1
- package/skills/release-publisher/SKILL.md +101 -0
- package/skills/scenario-test-designer/SKILL.md +27 -14
- package/skills/scenario-test-logic-designer/SKILL.md +10 -8
- package/skills/scenario-test-logic-designer/references/msw-patterns.md +3 -1
- package/skills/scenario-test-logic-designer/references/playwright-patterns.md +3 -1
- package/skills/skill-creator/SKILL.md +75 -332
- package/skills/story-implementor/SKILL.md +54 -0
- package/skills/story-mapper/SKILL.md +8 -4
- package/skills/story-writer/SKILL.md +14 -5
- package/skills/test-coverage-checker/SKILL.md +4 -6
- package/skills/uiux-designer/SKILL.md +4 -2
- package/skills/uiux-designer/references/uiux-design-template.md +4 -4
- package/skills/unit-designer/SKILL.md +11 -7
- package/skills/unit-test-designer/SKILL.md +25 -11
- package/skills/unit-test-logic-designer/SKILL.md +10 -8
- package/skills/unit-test-logic-designer/references/test-patterns.md +7 -1
- package/skills/doc-freshness-checker/SKILL.md +0 -140
- package/skills/implementation-planner/SKILL.md +0 -167
- 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 -104
package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
export interface StoryReflectionFileSystemPort {
|
|
5
5
|
listStoryDirectories(unitId: string): Promise<readonly string[]>;
|
|
6
6
|
storyAffectsUnit(storyId: string, unitId: string): Promise<boolean>;
|
|
7
|
+
/** WI のコミット(Work-Item trailer)が scripts/harness/{unitId}/{layer}/ 配下のソースを変更したか */
|
|
8
|
+
storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean>;
|
|
7
9
|
fileExists(path: string): Promise<boolean>;
|
|
8
10
|
/** product 文書内に @story-id {storyId} アノテーションが含まれているか */
|
|
9
11
|
fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean>;
|
|
@@ -49,6 +49,14 @@ export class StoryReflectionChecker {
|
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (
|
|
53
|
+
resolvedInception.isCrossWorkItem &&
|
|
54
|
+
productPath.endsWith("domain_model.md") &&
|
|
55
|
+
!(await this.fsPort.storyTouchesUnitLayer(storyId, unitId, "domain"))
|
|
56
|
+
) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
const hasAnnotation = await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
|
|
53
61
|
|
|
54
62
|
if (!hasAnnotation) {
|
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
* @work-item-id WI-115
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
7
8
|
import type { Dirent } from "node:fs";
|
|
8
9
|
import { access, readdir, readFile } from "node:fs/promises";
|
|
9
10
|
import path from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
10
12
|
import type { StoryReflectionFileSystemPort } from "../../domain/ports/story-reflection-file-system-port.js";
|
|
11
13
|
|
|
12
14
|
const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
|
|
13
15
|
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
14
16
|
const AFFECTS_INLINE_PATTERN = /^affects:\s*\[([^\]]*)\]\s*$/m;
|
|
15
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\/[^/]+\/[^/]+\//;
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
16
25
|
|
|
17
26
|
export interface FileSystemStoryReflectionAdapterDeps {
|
|
18
27
|
readonly rootDir: string;
|
|
@@ -34,6 +43,7 @@ export interface FileSystemStoryReflectionAdapterDeps {
|
|
|
34
43
|
export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
|
|
35
44
|
private readonly rootDir: string;
|
|
36
45
|
private readonly inceptionRoot: string;
|
|
46
|
+
private readonly changedPathsByStoryId = new Map<string, Promise<ReadonlySet<string>>>();
|
|
37
47
|
|
|
38
48
|
constructor(deps: FileSystemStoryReflectionAdapterDeps) {
|
|
39
49
|
this.rootDir = deps.rootDir;
|
|
@@ -110,7 +120,7 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
110
120
|
|
|
111
121
|
const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
|
|
112
122
|
if (frontmatter === undefined) {
|
|
113
|
-
return
|
|
123
|
+
return false;
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
|
|
@@ -119,12 +129,137 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
119
129
|
.filter((value) => value.length > 0);
|
|
120
130
|
|
|
121
131
|
if (affects === undefined) {
|
|
122
|
-
return
|
|
132
|
+
return false;
|
|
123
133
|
}
|
|
124
134
|
|
|
125
135
|
return affects.includes(unitId);
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
async storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean> {
|
|
139
|
+
const paths = await this.readChangedPathsForStory(storyId);
|
|
140
|
+
const prefix = `scripts/harness/${unitId}/${layer}/`;
|
|
141
|
+
|
|
142
|
+
for (const changedPath of paths) {
|
|
143
|
+
if (changedPath.startsWith(prefix)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private readChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
|
|
152
|
+
const cached = this.changedPathsByStoryId.get(storyId);
|
|
153
|
+
if (cached !== undefined) {
|
|
154
|
+
return cached;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const promise = this.fetchChangedPathsForStory(storyId);
|
|
158
|
+
this.changedPathsByStoryId.set(storyId, promise);
|
|
159
|
+
return promise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async fetchChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
|
|
163
|
+
try {
|
|
164
|
+
const { stdout } = await execFileAsync(
|
|
165
|
+
"git",
|
|
166
|
+
[
|
|
167
|
+
"log",
|
|
168
|
+
"--format=@@COMMIT@@%x00%B%x00",
|
|
169
|
+
"--name-only",
|
|
170
|
+
"--grep",
|
|
171
|
+
`Work-Item:.*\\b${storyId}\\b`,
|
|
172
|
+
],
|
|
173
|
+
{ cwd: this.rootDir, maxBuffer: 32 * 1024 * 1024 },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return await this.extractChangedPathsForStory(stdout, storyId);
|
|
177
|
+
} catch {
|
|
178
|
+
return new Set<string>();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private async extractChangedPathsForStory(output: string, storyId: string): Promise<ReadonlySet<string>> {
|
|
183
|
+
const changedPaths = new Set<string>();
|
|
184
|
+
const trailerPattern = new RegExp(`Work-Item:[^\\n]*\\b${storyId}\\b`);
|
|
185
|
+
|
|
186
|
+
for (const commitBlock of output.split("@@COMMIT@@\u0000")) {
|
|
187
|
+
if (commitBlock.length === 0) continue;
|
|
188
|
+
|
|
189
|
+
const bodyEnd = commitBlock.indexOf("\u0000");
|
|
190
|
+
if (bodyEnd === -1) continue;
|
|
191
|
+
|
|
192
|
+
const body = commitBlock.slice(0, bodyEnd);
|
|
193
|
+
if (!trailerPattern.test(body)) continue;
|
|
194
|
+
|
|
195
|
+
// 複数 Work-Item trailer を同梱するバッチコミットでは、changed paths が
|
|
196
|
+
// 全 trailer WI に一律帰属してしまう(over-attribution)。ソースパスに限り
|
|
197
|
+
// ファイルの帰属タグで storyId への帰属を絞り込む(WI-251)。
|
|
198
|
+
const isMultiWorkItemCommit = this.countWorkItemTrailers(body) >= 2;
|
|
199
|
+
const nameOnly = commitBlock.slice(bodyEnd + 1);
|
|
200
|
+
for (const changedPath of nameOnly.split(/\r?\n/)) {
|
|
201
|
+
const trimmedPath = changedPath.trim();
|
|
202
|
+
if (trimmedPath.length === 0 || trimmedPath === "@@COMMIT@@") continue;
|
|
203
|
+
|
|
204
|
+
if (await this.pathAttributesToStory(trimmedPath, storyId, isMultiWorkItemCommit)) {
|
|
205
|
+
changedPaths.add(trimmedPath);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return changedPaths;
|
|
211
|
+
}
|
|
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
|
+
|
|
128
263
|
async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
|
|
129
264
|
const absolutePath = path.join(this.rootDir, productPath);
|
|
130
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[] {
|
|
@@ -173,9 +171,12 @@ export function renderSkillForModelDelegation(content: string, policy: ModelDele
|
|
|
173
171
|
return content
|
|
174
172
|
.replace(/^model:\s+\S+\n/gm, "")
|
|
175
173
|
.replace(/^review:\s+\S+\n/gm, "")
|
|
176
|
-
.replaceAll(
|
|
174
|
+
.replaceAll(
|
|
175
|
+
"委任先モデルに委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)",
|
|
176
|
+
"メインセッションが成果物を生成する",
|
|
177
|
+
)
|
|
177
178
|
.replaceAll("Opus が", "メインセッションが")
|
|
178
|
-
.replaceAll("
|
|
179
|
+
.replaceAll("委任先モデル", "メインセッション")
|
|
179
180
|
.replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
|
|
180
181
|
}
|
|
181
182
|
|
|
@@ -726,10 +727,7 @@ export async function deployHuskyCommitMsgHook(
|
|
|
726
727
|
return { created: true, path: targetPath };
|
|
727
728
|
}
|
|
728
729
|
|
|
729
|
-
export async function deployHuskyPrePushHook(
|
|
730
|
-
harnessRoot: string,
|
|
731
|
-
projectRoot: string,
|
|
732
|
-
): Promise<DeployHuskyHookResult> {
|
|
730
|
+
export async function deployHuskyPrePushHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
|
|
733
731
|
const targetPath = join(projectRoot, ".husky", "pre-push");
|
|
734
732
|
|
|
735
733
|
try {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit skill-quality
|
|
4
|
-
* @work-item-id WI-212
|
|
4
|
+
* @work-item-id WI-212, WI-241
|
|
5
5
|
*/
|
|
6
6
|
import { SkillStructure } from '../value-objects/skill-structure.js';
|
|
7
7
|
import { SkillValidationResult } from '../value-objects/skill-validation-result.js';
|
|
8
8
|
import type { SkillFileReaderPort } from '../ports/skill-file-reader-port.js';
|
|
9
9
|
import type { SectionName } from '../types/section-name.js';
|
|
10
|
+
import type { SkillKind } from '../types/skill-kind.js';
|
|
10
11
|
|
|
11
12
|
export class SkillStructureValidator {
|
|
12
13
|
constructor(private readonly skillFileReaderPort: SkillFileReaderPort) {}
|
|
@@ -14,7 +15,8 @@ export class SkillStructureValidator {
|
|
|
14
15
|
async validate(skillFilePath: string): Promise<SkillValidationResult> {
|
|
15
16
|
const rawContent = await this.skillFileReaderPort.read(skillFilePath);
|
|
16
17
|
const actualSections = this.extractSections(rawContent);
|
|
17
|
-
const
|
|
18
|
+
const kind = this.extractKind(rawContent);
|
|
19
|
+
const structure = SkillStructure.forKind(kind);
|
|
18
20
|
const missingSections = structure.getMissingSections(actualSections);
|
|
19
21
|
|
|
20
22
|
if (missingSections.length === 0) {
|
|
@@ -74,6 +76,15 @@ export class SkillStructureValidator {
|
|
|
74
76
|
return sections;
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
private extractKind(content: string): SkillKind {
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
82
|
+
if (lines[0]?.trim() !== '---' || closingDelimiterIndex < 0) return 'lifecycle';
|
|
83
|
+
|
|
84
|
+
const frontmatterLines = lines.slice(1, closingDelimiterIndex);
|
|
85
|
+
return frontmatterLines.some((line) => /^\s*kind\s*:\s*advisory\s*$/.test(line)) ? 'advisory' : 'lifecycle';
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
private hasLanguageMetadata(lines: readonly string[]): boolean {
|
|
78
89
|
const closingDelimiterIndex = lines.findIndex((line, index) => index > 0 && line.trim() === '---');
|
|
79
90
|
if (closingDelimiterIndex < 0) return false;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer domain
|
|
3
3
|
* @unit skill-quality
|
|
4
|
-
* @work-item-id WI-212
|
|
4
|
+
* @work-item-id WI-212, WI-241
|
|
5
5
|
*/
|
|
6
6
|
import type { SectionName } from '../types/section-name.js';
|
|
7
|
+
import type { SkillKind } from '../types/skill-kind.js';
|
|
7
8
|
|
|
8
|
-
const REQUIRED_SECTIONS: readonly SectionName[] = [
|
|
9
|
+
const REQUIRED_SECTIONS: readonly SectionName[] = Object.freeze([
|
|
9
10
|
'frontmatter',
|
|
10
11
|
'languageMetadata',
|
|
11
12
|
'purpose',
|
|
@@ -13,7 +14,13 @@ const REQUIRED_SECTIONS: readonly SectionName[] = [
|
|
|
13
14
|
'outputs',
|
|
14
15
|
'prerequisites',
|
|
15
16
|
'executionFlow',
|
|
16
|
-
];
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const ADVISORY_REQUIRED_SECTIONS: readonly SectionName[] = Object.freeze([
|
|
20
|
+
'frontmatter',
|
|
21
|
+
'languageMetadata',
|
|
22
|
+
'purpose',
|
|
23
|
+
]);
|
|
17
24
|
|
|
18
25
|
export class SkillStructure {
|
|
19
26
|
readonly requiredSections: readonly SectionName[];
|
|
@@ -23,13 +30,22 @@ export class SkillStructure {
|
|
|
23
30
|
Object.freeze(this);
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
private static
|
|
33
|
+
private static _instances: Partial<Record<SkillKind, SkillStructure>> = {};
|
|
27
34
|
|
|
28
|
-
static
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
static forKind(kind: SkillKind): SkillStructure {
|
|
36
|
+
const cached = SkillStructure._instances[kind];
|
|
37
|
+
if (cached) {
|
|
38
|
+
return cached;
|
|
31
39
|
}
|
|
32
|
-
|
|
40
|
+
|
|
41
|
+
const requiredSections = kind === 'advisory' ? ADVISORY_REQUIRED_SECTIONS : REQUIRED_SECTIONS;
|
|
42
|
+
const instance = new SkillStructure(requiredSections);
|
|
43
|
+
SkillStructure._instances[kind] = instance;
|
|
44
|
+
return instance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static default(): SkillStructure {
|
|
48
|
+
return SkillStructure.forKind('lifecycle');
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
getMissingSections(actualSections: readonly SectionName[]): readonly SectionName[] {
|