phasegate 0.181.0 → 0.191.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/docs/guide/skills-overview.md +10 -9
- package/package.json +1 -1
- package/scripts/harness/biome-ast-engine/infrastructure/parsers/comment-density-parser.ts +51 -10
- package/scripts/harness/ci-governance/composition-root.ts +1 -1
- package/scripts/harness/ci-governance/infrastructure/adapters/adr-foundation-existence-adapter.ts +26 -2
- package/scripts/harness/ci-governance/infrastructure/adapters/harness-api-command-existence-adapter.ts +74 -1
- 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 +75 -2
- package/scripts/harness/setup/skill-deployer.ts +6 -6
- 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/skills/cascade-updater/SKILL.md +3 -3
- package/skills/codebase-mapper/SKILL.md +17 -5
- package/skills/codex-delegator/SKILL.md +19 -4
- 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/doc-freshness-checker/SKILL.md +8 -0
- package/skills/domain-designer/SKILL.md +4 -2
- package/skills/engineering-perspective/SKILL.md +3 -0
- package/skills/environment-designer/SKILL.md +8 -6
- package/skills/implementation-planner/SKILL.md +12 -6
- package/skills/implementation-readiness-checker/SKILL.md +9 -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 +12 -2
- package/skills/phasegate-toolkit-guide/SKILL.md +3 -0
- package/skills/pointer-validator/SKILL.md +8 -0
- package/skills/quick-implementor/SKILL.md +7 -5
- 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 +8 -5
- package/skills/story-implementor/SKILL.md +2 -0
- package/skills/story-mapper/SKILL.md +4 -4
- package/skills/story-writer/SKILL.md +5 -5
- package/skills/test-coverage-checker/SKILL.md +4 -6
- package/skills/uiux-designer/SKILL.md +4 -2
- package/skills/unit-designer/SKILL.md +8 -6
- 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
|
@@ -37,15 +37,16 @@ Artifacts are placed in `inception/{unit}/{US-XXX}/`.
|
|
|
37
37
|
Skills run per User Story:
|
|
38
38
|
|
|
39
39
|
1. `/logical-designer` (US-specific mode) — Story-scoped logical design refinement.
|
|
40
|
-
2. `/
|
|
41
|
-
3. `/
|
|
42
|
-
4. `/
|
|
43
|
-
5. `/
|
|
44
|
-
6. `/
|
|
45
|
-
7. `/
|
|
46
|
-
8. `/
|
|
47
|
-
9. `/
|
|
48
|
-
10. `/
|
|
40
|
+
2. `/scenario-test-designer` — E2E scenario test case design. Its `scenario_test_design.md` output is a required input of `/uiux-designer`.
|
|
41
|
+
3. `/uiux-designer` — Final UI/UX definition incorporating test cases, logical design, and existing UI.
|
|
42
|
+
4. `/unit-test-designer` — Unit test case design from domain model.
|
|
43
|
+
5. `/it-test-designer` — Integration test case design from logical design.
|
|
44
|
+
6. `/test-coverage-checker` — Coverage verification of all three test case designs before test logic design.
|
|
45
|
+
7. `/unit-test-logic-designer` — Vitest implementation logic with pseudocode.
|
|
46
|
+
8. `/it-test-logic-designer` — IT Vitest implementation logic with pseudocode.
|
|
47
|
+
9. `/scenario-test-logic-designer` — Playwright E2E test implementation logic.
|
|
48
|
+
10. `/implementation-readiness-checker` — Automated pre-implementation readiness verification.
|
|
49
|
+
11. `/story-implementor` — TDD implementation with atomic commits.
|
|
49
50
|
|
|
50
51
|
### Phase Dependency Rules
|
|
51
52
|
|
package/package.json
CHANGED
|
@@ -11,22 +11,37 @@ export type CommentDensityResult = {
|
|
|
11
11
|
|
|
12
12
|
const SINGLE_LINE_COMMENT = /^\s*\/\//;
|
|
13
13
|
const MULTI_LINE_COMMENT_START = /^\s*\/\*/;
|
|
14
|
+
const DOC_COMMENT_START = /^\s*\/\*\*/;
|
|
14
15
|
const MULTI_LINE_COMMENT_END = /\*\//;
|
|
15
16
|
const BLANK_LINE = /^\s*$/;
|
|
17
|
+
// L1 ルール (require-unit-comment / require-layer-comment) が必須化する
|
|
18
|
+
// 先頭メタデータヘッダのタグ行。ファイル冒頭の連続コメント領域でのみ効力を持つ。
|
|
19
|
+
const METADATA_HEADER_TAG = /^\s*\/\/\s*@(unit|layer|work-item-id|story|story-id|issue-id|module|feature|epic)\b/;
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* ソースコードのコメント密度と繰り返しブロック数を算出する。
|
|
19
23
|
*
|
|
20
|
-
* - commentLineCount
|
|
21
|
-
*
|
|
22
|
-
* -
|
|
24
|
+
* WI-239: commentLineCount(密度分子)は「密度に寄与する narrative コメント行数」
|
|
25
|
+
* を表すよう意味を再定義した。次は分子から除外する:
|
|
26
|
+
* - 先頭の必須メタデータヘッダ行(`// @unit` / `// @layer` / `// @work-item-id` /
|
|
27
|
+
* `// @story` 等。ファイル冒頭の連続コメント領域に限る)
|
|
28
|
+
* - `/** … *\/` doc-comment ブロック(ブロック全体)
|
|
29
|
+
* 分子に残すのは narrative な `//` 行コメントと非 doc の `/* … *\/` ブロックのみ。
|
|
30
|
+
*
|
|
31
|
+
* - commentLineCount: 密度分子となる narrative コメント行数
|
|
32
|
+
* - logicalLineCount: 空行を除いた行数(分母。定義不変)
|
|
33
|
+
* - repeatedCommentBlocks: 連続する同一 narrative コメントブロックの検出回数
|
|
23
34
|
*/
|
|
24
35
|
export const parseCommentDensity = (sourceCode: string): CommentDensityResult => {
|
|
25
|
-
const lines = sourceCode.split(
|
|
36
|
+
const lines = sourceCode.split("\n");
|
|
26
37
|
|
|
27
38
|
let commentLineCount = 0;
|
|
28
39
|
let logicalLineCount = 0;
|
|
29
40
|
let insideBlockComment = false;
|
|
41
|
+
let insideDocComment = false;
|
|
42
|
+
// ファイル冒頭の連続したメタデータヘッダ領域内かどうか。
|
|
43
|
+
// 最初のコード行または非メタデータコメントが現れた時点で false になる。
|
|
44
|
+
let insideHeaderZone = true;
|
|
30
45
|
|
|
31
46
|
const commentBlocks: string[] = [];
|
|
32
47
|
let currentBlock: string[] = [];
|
|
@@ -41,11 +56,14 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
|
|
|
41
56
|
logicalLineCount += 1;
|
|
42
57
|
|
|
43
58
|
if (insideBlockComment) {
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
if (!insideDocComment) {
|
|
60
|
+
commentLineCount += 1;
|
|
61
|
+
currentBlock.push(line.trim());
|
|
62
|
+
}
|
|
46
63
|
|
|
47
64
|
if (MULTI_LINE_COMMENT_END.test(line)) {
|
|
48
65
|
insideBlockComment = false;
|
|
66
|
+
insideDocComment = false;
|
|
49
67
|
flushBlock(currentBlock, commentBlocks);
|
|
50
68
|
currentBlock = [];
|
|
51
69
|
}
|
|
@@ -54,25 +72,48 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
|
|
|
54
72
|
}
|
|
55
73
|
|
|
56
74
|
if (SINGLE_LINE_COMMENT.test(line)) {
|
|
75
|
+
const isMetadataHeader = insideHeaderZone && METADATA_HEADER_TAG.test(line);
|
|
76
|
+
|
|
77
|
+
if (isMetadataHeader) {
|
|
78
|
+
// メタデータヘッダは分子・反復検出のいずれにも寄与しない。
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// narrative な行コメントに到達 → ヘッダ領域は終了。
|
|
83
|
+
insideHeaderZone = false;
|
|
57
84
|
commentLineCount += 1;
|
|
58
85
|
currentBlock.push(line.trim());
|
|
59
86
|
continue;
|
|
60
87
|
}
|
|
61
88
|
|
|
62
89
|
if (MULTI_LINE_COMMENT_START.test(line)) {
|
|
90
|
+
insideHeaderZone = false;
|
|
91
|
+
const isDocComment = DOC_COMMENT_START.test(line);
|
|
92
|
+
const isClosed = MULTI_LINE_COMMENT_END.test(line);
|
|
93
|
+
|
|
94
|
+
if (isDocComment) {
|
|
95
|
+
// doc-comment ブロックは分子・反復検出のいずれにも寄与しない。
|
|
96
|
+
if (!isClosed) {
|
|
97
|
+
insideBlockComment = true;
|
|
98
|
+
insideDocComment = true;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
63
103
|
commentLineCount += 1;
|
|
64
|
-
insideBlockComment = true;
|
|
65
104
|
currentBlock.push(line.trim());
|
|
66
105
|
|
|
67
|
-
if (
|
|
68
|
-
insideBlockComment = false;
|
|
106
|
+
if (isClosed) {
|
|
69
107
|
flushBlock(currentBlock, commentBlocks);
|
|
70
108
|
currentBlock = [];
|
|
109
|
+
} else {
|
|
110
|
+
insideBlockComment = true;
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
continue;
|
|
74
114
|
}
|
|
75
115
|
|
|
116
|
+
insideHeaderZone = false;
|
|
76
117
|
flushBlock(currentBlock, commentBlocks);
|
|
77
118
|
currentBlock = [];
|
|
78
119
|
}
|
|
@@ -86,7 +127,7 @@ export const parseCommentDensity = (sourceCode: string): CommentDensityResult =>
|
|
|
86
127
|
|
|
87
128
|
const flushBlock = (current: string[], blocks: string[]): void => {
|
|
88
129
|
if (current.length > 0) {
|
|
89
|
-
blocks.push(current.join(
|
|
130
|
+
blocks.push(current.join("\n"));
|
|
90
131
|
}
|
|
91
132
|
};
|
|
92
133
|
|
|
@@ -84,7 +84,7 @@ export function buildCiGovernance(
|
|
|
84
84
|
const templateRendererPort = new YamlTemplateRendererAdapter(harnessRoot);
|
|
85
85
|
const fileExistencePort = new FileSystemExistenceAdapter(baseDir);
|
|
86
86
|
const commandExistencePort = new HarnessApiCommandExistenceAdapter();
|
|
87
|
-
const adrExistencePort = new AdrFoundationExistenceAdapter();
|
|
87
|
+
const adrExistencePort = new AdrFoundationExistenceAdapter(baseDir);
|
|
88
88
|
const agentsMdPort = new AgentsMdFileAdapter(baseDir);
|
|
89
89
|
const agentContextDocumentPort = new AgentContextFileAdapter(baseDir, harnessRoot);
|
|
90
90
|
const lessonArtifactReaderPort = new LessonArtifactFileReaderAdapter(baseDir);
|
package/scripts/harness/ci-governance/infrastructure/adapters/adr-foundation-existence-adapter.ts
CHANGED
|
@@ -5,12 +5,36 @@
|
|
|
5
5
|
* AdrExistencePort実装
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import * as path from 'node:path';
|
|
8
9
|
import type { AdrExistencePort } from '../../domain/ports/adr-existence-port.js';
|
|
9
10
|
|
|
10
11
|
export class AdrFoundationExistenceAdapter implements AdrExistencePort {
|
|
11
|
-
|
|
12
|
+
private repositoryPromise: Promise<{
|
|
13
|
+
exists(id: { readonly value: string }): Promise<boolean>;
|
|
14
|
+
}> | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(private readonly rootDir: string = process.cwd()) {}
|
|
12
17
|
|
|
13
18
|
async exists(adrId: string): Promise<boolean> {
|
|
14
|
-
|
|
19
|
+
try {
|
|
20
|
+
const { AdrId } = await import('../../../adr-foundation/domain/value-objects/adr-id.js');
|
|
21
|
+
const id = AdrId.create(adrId);
|
|
22
|
+
const repository = await this.getRepository();
|
|
23
|
+
return repository.exists(id);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async getRepository(): Promise<{
|
|
30
|
+
exists(id: { readonly value: string }): Promise<boolean>;
|
|
31
|
+
}> {
|
|
32
|
+
if (!this.repositoryPromise) {
|
|
33
|
+
this.repositoryPromise = (async () => {
|
|
34
|
+
const { createAdrFoundationModule } = await import('../../../adr-foundation/composition-root.js');
|
|
35
|
+
return createAdrFoundationModule(path.join(this.rootDir, 'docs', 'ADR')).adrRepository;
|
|
36
|
+
})();
|
|
37
|
+
}
|
|
38
|
+
return this.repositoryPromise;
|
|
15
39
|
}
|
|
16
40
|
}
|
|
@@ -3,10 +3,83 @@
|
|
|
3
3
|
|
|
4
4
|
import type { CommandExistencePort } from '../../domain/ports/command-existence-port.js';
|
|
5
5
|
|
|
6
|
+
const KNOWN_HARNESS_COMMANDS: readonly string[] = [
|
|
7
|
+
'baseline',
|
|
8
|
+
'bypass:audit',
|
|
9
|
+
'check-change-category',
|
|
10
|
+
'check-phase-gate',
|
|
11
|
+
'ci-check',
|
|
12
|
+
'ci:auto-refresh-agent-context',
|
|
13
|
+
'ci:check-repetition',
|
|
14
|
+
'ci:generate-template',
|
|
15
|
+
'ci:migrate-agents-md',
|
|
16
|
+
'commit-msg',
|
|
17
|
+
'complete-check',
|
|
18
|
+
'config:plan',
|
|
19
|
+
'delegate-sonnet',
|
|
20
|
+
'disable-feature',
|
|
21
|
+
'doctor',
|
|
22
|
+
'emit-agent-rules',
|
|
23
|
+
'enable-feature',
|
|
24
|
+
'hook',
|
|
25
|
+
'init',
|
|
26
|
+
'install',
|
|
27
|
+
'lint',
|
|
28
|
+
'list-adrs',
|
|
29
|
+
'list-errors',
|
|
30
|
+
'list-features',
|
|
31
|
+
'migrate',
|
|
32
|
+
'p2:check-agent-context',
|
|
33
|
+
'p2:check-freshness',
|
|
34
|
+
'p2:check-initial-creation',
|
|
35
|
+
'p2:generate-e2e-template',
|
|
36
|
+
'p2:validate-pointers',
|
|
37
|
+
'phasegate:attest',
|
|
38
|
+
'phasegate:check-phase',
|
|
39
|
+
'phasegate:check-ready',
|
|
40
|
+
'phasegate:ci-check',
|
|
41
|
+
'phasegate:complete-check',
|
|
42
|
+
'phasegate:detect-drift',
|
|
43
|
+
'phasegate:generate-matrix',
|
|
44
|
+
'phasegate:impact-analysis',
|
|
45
|
+
'phasegate:lint',
|
|
46
|
+
'phasegate:status',
|
|
47
|
+
'phasegate:verify-attestation',
|
|
48
|
+
'pre-commit',
|
|
49
|
+
'reconcile',
|
|
50
|
+
'refresh-claude-md',
|
|
51
|
+
'regression:analyze-migration',
|
|
52
|
+
'regression:configure-ci-gate',
|
|
53
|
+
'regression:migrate-v0-tests',
|
|
54
|
+
'regression:run-agent-guard',
|
|
55
|
+
'regression:run-gng-gate',
|
|
56
|
+
'regression:run-k-requirements',
|
|
57
|
+
'regression:run-k14-k15',
|
|
58
|
+
'render-errors',
|
|
59
|
+
'scaffold-design',
|
|
60
|
+
'scaffold-wi',
|
|
61
|
+
'session',
|
|
62
|
+
'setup:agent',
|
|
63
|
+
'skill:apply-cascade-update',
|
|
64
|
+
'skill:check-coverage',
|
|
65
|
+
'skill:collect-lessons',
|
|
66
|
+
'skill:execute-tdd-cycle',
|
|
67
|
+
'skill:validate-structure',
|
|
68
|
+
'skills',
|
|
69
|
+
'status',
|
|
70
|
+
'uninstall',
|
|
71
|
+
'update-skills',
|
|
72
|
+
'validate',
|
|
73
|
+
'validate-adr',
|
|
74
|
+
'validate-fix',
|
|
75
|
+
'validate-metadata',
|
|
76
|
+
'work-items:status',
|
|
77
|
+
];
|
|
78
|
+
|
|
6
79
|
export class HarnessApiCommandExistenceAdapter implements CommandExistencePort {
|
|
7
80
|
private readonly knownCommands: Set<string>;
|
|
8
81
|
|
|
9
|
-
constructor(knownCommands: string[] = []) {
|
|
82
|
+
constructor(knownCommands: string[] = [...KNOWN_HARNESS_COMMANDS]) {
|
|
10
83
|
this.knownCommands = new Set(knownCommands);
|
|
11
84
|
}
|
|
12
85
|
|
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,18 @@
|
|
|
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
|
+
const execFileAsync = promisify(execFile);
|
|
16
19
|
|
|
17
20
|
export interface FileSystemStoryReflectionAdapterDeps {
|
|
18
21
|
readonly rootDir: string;
|
|
@@ -34,6 +37,7 @@ export interface FileSystemStoryReflectionAdapterDeps {
|
|
|
34
37
|
export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
|
|
35
38
|
private readonly rootDir: string;
|
|
36
39
|
private readonly inceptionRoot: string;
|
|
40
|
+
private readonly changedPathsByStoryId = new Map<string, Promise<ReadonlySet<string>>>();
|
|
37
41
|
|
|
38
42
|
constructor(deps: FileSystemStoryReflectionAdapterDeps) {
|
|
39
43
|
this.rootDir = deps.rootDir;
|
|
@@ -110,7 +114,7 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
110
114
|
|
|
111
115
|
const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
|
|
112
116
|
if (frontmatter === undefined) {
|
|
113
|
-
return
|
|
117
|
+
return false;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
|
|
@@ -119,12 +123,81 @@ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSyst
|
|
|
119
123
|
.filter((value) => value.length > 0);
|
|
120
124
|
|
|
121
125
|
if (affects === undefined) {
|
|
122
|
-
return
|
|
126
|
+
return false;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
return affects.includes(unitId);
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
async storyTouchesUnitLayer(storyId: string, unitId: string, layer: string): Promise<boolean> {
|
|
133
|
+
const paths = await this.readChangedPathsForStory(storyId);
|
|
134
|
+
const prefix = `scripts/harness/${unitId}/${layer}/`;
|
|
135
|
+
|
|
136
|
+
for (const changedPath of paths) {
|
|
137
|
+
if (changedPath.startsWith(prefix)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private readChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
|
|
146
|
+
const cached = this.changedPathsByStoryId.get(storyId);
|
|
147
|
+
if (cached !== undefined) {
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const promise = this.fetchChangedPathsForStory(storyId);
|
|
152
|
+
this.changedPathsByStoryId.set(storyId, promise);
|
|
153
|
+
return promise;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async fetchChangedPathsForStory(storyId: string): Promise<ReadonlySet<string>> {
|
|
157
|
+
try {
|
|
158
|
+
const { stdout } = await execFileAsync(
|
|
159
|
+
"git",
|
|
160
|
+
[
|
|
161
|
+
"log",
|
|
162
|
+
"--format=@@COMMIT@@%x00%B%x00",
|
|
163
|
+
"--name-only",
|
|
164
|
+
"--grep",
|
|
165
|
+
`Work-Item:.*\\b${storyId}\\b`,
|
|
166
|
+
],
|
|
167
|
+
{ cwd: this.rootDir, maxBuffer: 32 * 1024 * 1024 },
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return this.extractChangedPathsForStory(stdout, storyId);
|
|
171
|
+
} catch {
|
|
172
|
+
return new Set<string>();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private extractChangedPathsForStory(output: string, storyId: string): ReadonlySet<string> {
|
|
177
|
+
const changedPaths = new Set<string>();
|
|
178
|
+
const trailerPattern = new RegExp(`Work-Item:[^\\n]*\\b${storyId}\\b`);
|
|
179
|
+
|
|
180
|
+
for (const commitBlock of output.split("@@COMMIT@@\u0000")) {
|
|
181
|
+
if (commitBlock.length === 0) continue;
|
|
182
|
+
|
|
183
|
+
const bodyEnd = commitBlock.indexOf("\u0000");
|
|
184
|
+
if (bodyEnd === -1) continue;
|
|
185
|
+
|
|
186
|
+
const body = commitBlock.slice(0, bodyEnd);
|
|
187
|
+
if (!trailerPattern.test(body)) continue;
|
|
188
|
+
|
|
189
|
+
const nameOnly = commitBlock.slice(bodyEnd + 1);
|
|
190
|
+
for (const changedPath of nameOnly.split(/\r?\n/)) {
|
|
191
|
+
const trimmedPath = changedPath.trim();
|
|
192
|
+
if (trimmedPath.length > 0 && trimmedPath !== "@@COMMIT@@") {
|
|
193
|
+
changedPaths.add(trimmedPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return changedPaths;
|
|
199
|
+
}
|
|
200
|
+
|
|
128
201
|
async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
|
|
129
202
|
const absolutePath = path.join(this.rootDir, productPath);
|
|
130
203
|
|
|
@@ -173,9 +173,12 @@ export function renderSkillForModelDelegation(content: string, policy: ModelDele
|
|
|
173
173
|
return content
|
|
174
174
|
.replace(/^model:\s+\S+\n/gm, "")
|
|
175
175
|
.replace(/^review:\s+\S+\n/gm, "")
|
|
176
|
-
.replaceAll(
|
|
176
|
+
.replaceAll(
|
|
177
|
+
"委任先モデルに委任して成果物を生成する(`npx phasegate delegate-sonnet` 経由)",
|
|
178
|
+
"メインセッションが成果物を生成する",
|
|
179
|
+
)
|
|
177
180
|
.replaceAll("Opus が", "メインセッションが")
|
|
178
|
-
.replaceAll("
|
|
181
|
+
.replaceAll("委任先モデル", "メインセッション")
|
|
179
182
|
.replaceAll("`npx phasegate delegate-sonnet`", "メインセッション");
|
|
180
183
|
}
|
|
181
184
|
|
|
@@ -726,10 +729,7 @@ export async function deployHuskyCommitMsgHook(
|
|
|
726
729
|
return { created: true, path: targetPath };
|
|
727
730
|
}
|
|
728
731
|
|
|
729
|
-
export async function deployHuskyPrePushHook(
|
|
730
|
-
harnessRoot: string,
|
|
731
|
-
projectRoot: string,
|
|
732
|
-
): Promise<DeployHuskyHookResult> {
|
|
732
|
+
export async function deployHuskyPrePushHook(harnessRoot: string, projectRoot: string): Promise<DeployHuskyHookResult> {
|
|
733
733
|
const targetPath = join(projectRoot, ".husky", "pre-push");
|
|
734
734
|
|
|
735
735
|
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[] {
|
|
@@ -12,7 +12,7 @@ languages: [typescript]
|
|
|
12
12
|
|
|
13
13
|
## storyReflection ゲート通過の標準手段
|
|
14
14
|
|
|
15
|
-
`phaseDependencies.storyReflection`(ADR-013)は、`docs/inception/{unit}/{storyId}/` 配下の US/issue 設計が `docs/product/construction/{unit}/` の累積設計文書に反映されていない場合、`src/{unit}
|
|
15
|
+
`phaseDependencies.storyReflection`(ADR-013)は、`docs/inception/{unit}/{storyId}/` 配下の US/issue 設計が `docs/product/construction/{unit}/` の累積設計文書に反映されていない場合、`src/{unit}/*`(phasegate 自リポジトリ(dogfood)では `scripts/harness/{unit}/*`)への Write/Edit(Bash 迂回経由の書き込みも含む)を pre-tool-use hook でブロックする。
|
|
16
16
|
|
|
17
17
|
**cascade-updater はこの storyReflection ゲートを通過する標準手段である。**
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ languages: [typescript]
|
|
|
21
21
|
- `minimal` プリセットでは storyReflection が無効化されるためゲート連動しないが、cascade-updater による設計整合性の維持は推奨される
|
|
22
22
|
- Quick Mode (`relaxedGates: ["phase-gate"]`) 時は storyReflection も緩和されるため、cascade-updater 実行の必須性は緩まる(ただし品質維持のため実行は推奨)
|
|
23
23
|
|
|
24
|
-
詳細は ADR-013 および `docs/guide/configuration.md` の storyReflection
|
|
24
|
+
詳細は ADR-013 および `docs/guide/configuration.md` の storyReflection セクションを参照(consumer プロジェクトでは `node_modules/phasegate/docs/ADR/...` / `node_modules/phasegate/docs/guide/...`、phasegate 自リポジトリでは `docs/...` を参照する)。
|
|
25
25
|
|
|
26
26
|
## 前提条件チェック
|
|
27
27
|
|
|
@@ -120,7 +120,7 @@ languages: [typescript]
|
|
|
120
120
|
## Phase 3: レビュー(Opus review)
|
|
121
121
|
|
|
122
122
|
### 実行主体
|
|
123
|
-
メインセッション(
|
|
123
|
+
メインセッション(model-routing.md の Architect ロール)が実行する。Sonnetへの再委任は行わない。
|
|
124
124
|
|
|
125
125
|
### レビュー手順
|
|
126
126
|
1. Sonnetが出力したファイルを読み込む
|
|
@@ -8,9 +8,21 @@ languages: [typescript]
|
|
|
8
8
|
|
|
9
9
|
# Codebase Mapper
|
|
10
10
|
|
|
11
|
+
## 目的
|
|
12
|
+
|
|
11
13
|
全ソースファイルの `@unit`/`@layer` アノテーションとimportグラフを解析し、
|
|
12
14
|
コードベースの構造マップを生成するスキル。
|
|
13
15
|
|
|
16
|
+
## 入力
|
|
17
|
+
|
|
18
|
+
- 解析対象ソース: `phasegate.config.json` の `sourceDir` 配下の `**/*.ts`(`__tests__/`・`*.test.ts` は除外)
|
|
19
|
+
- 各ソースファイルの `@unit` / `@layer` アノテーションと `import` 文(Unit間依存グラフの構築に使用)
|
|
20
|
+
|
|
21
|
+
## 前提条件
|
|
22
|
+
|
|
23
|
+
- `phasegate.config.json` に `sourceDir` が設定されていること
|
|
24
|
+
- 解析対象ソースに `@unit` / `@layer` アノテーションが付与されていること(欠落ファイルは後述「アノテーション欠落の対処」で報告される)
|
|
25
|
+
|
|
14
26
|
## 出力物
|
|
15
27
|
|
|
16
28
|
`{constructionDir}/codebase-map.md` — Unit・レイヤー分布・依存関係の可視化文書(パスは `phasegate.config.json` で設定)
|
|
@@ -99,7 +111,7 @@ Unit間依存グラフを深さ優先探索でサイクル検出する。
|
|
|
99
111
|
|
|
100
112
|
| Unit | ファイル数 | レイヤー分布 |
|
|
101
113
|
|------|----------|-----------|
|
|
102
|
-
|
|
|
114
|
+
| unit-a | 12 | domain:4, app:4, infra:2, pres:2 |
|
|
103
115
|
| ...
|
|
104
116
|
|
|
105
117
|
---
|
|
@@ -107,11 +119,11 @@ Unit間依存グラフを深さ優先探索でサイクル検出する。
|
|
|
107
119
|
## Unit 間依存グラフ
|
|
108
120
|
|
|
109
121
|
```
|
|
110
|
-
|
|
111
|
-
└── depends on:
|
|
122
|
+
unit-a
|
|
123
|
+
└── depends on: unit-b, unit-c, ...
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
└── depends on:
|
|
125
|
+
unit-c
|
|
126
|
+
└── depends on: unit-d, unit-e
|
|
115
127
|
```
|
|
116
128
|
|
|
117
129
|
---
|