phasegate 0.91.0 → 0.107.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.ja.md +15 -7
  3. package/README.md +24 -4
  4. package/docs/ADR/ADR-015-architecture-preset.md +183 -0
  5. package/docs/guide/codex-integration.md +7 -2
  6. package/docs/guide/installation.md +10 -2
  7. package/docs/guide/preset-selection.md +170 -0
  8. package/docs/guide/quick-vs-full-mode.md +3 -3
  9. package/docs/guide/retrofit-adoption.md +19 -2
  10. package/docs/guide/skills-overview.md +1 -1
  11. package/package.json +7 -1
  12. package/scripts/harness/agent-integration/application/usecases/handle-pre-tool-use-usecase.ts +108 -100
  13. package/scripts/harness/agent-integration/domain/ports/phase-gate-query-port.ts +3 -3
  14. package/scripts/harness/agent-integration/domain/value-objects/write-target-scope.ts +26 -30
  15. package/scripts/harness/agent-integration/infrastructure/adapters/file-system-story-reflection-query-adapter.ts +6 -2
  16. package/scripts/harness/biome-ast-engine/application/dto/analyze-import-graph-input.ts +3 -0
  17. package/scripts/harness/biome-ast-engine/application/dto/resolve-enabled-rules-output.ts +2 -0
  18. package/scripts/harness/biome-ast-engine/application/mappers/resolve-enabled-rules-output-mapper.ts +4 -1
  19. package/scripts/harness/biome-ast-engine/application/usecases/analyze-import-graph-usecase.ts +4 -1
  20. package/scripts/harness/biome-ast-engine/application/usecases/execute-lint-usecase.ts +2 -0
  21. package/scripts/harness/biome-ast-engine/application/usecases/resolve-enabled-rules-usecase.ts +31 -3
  22. package/scripts/harness/biome-ast-engine/composition-root.ts +10 -2
  23. package/scripts/harness/biome-ast-engine/domain/ports/rule-config-provider-port.ts +16 -0
  24. package/scripts/harness/biome-ast-engine/domain/ports/source-module-analyzer-port.ts +5 -1
  25. package/scripts/harness/biome-ast-engine/domain/services/lint-runner.ts +5 -1
  26. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +38 -0
  27. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-boundary.ts +7 -8
  28. package/scripts/harness/biome-ast-engine/domain/value-objects/layer-name.ts +15 -23
  29. package/scripts/harness/biome-ast-engine/domain/value-objects/source-module-snapshot.ts +11 -4
  30. package/scripts/harness/biome-ast-engine/infrastructure/adapters/harness-config-provider-adapter.ts +29 -3
  31. package/scripts/harness/biome-ast-engine/infrastructure/adapters/typescript-source-module-analyzer-adapter.ts +22 -15
  32. package/scripts/harness/biome-ast-engine/infrastructure/mappers/source-module-snapshot-mapper.ts +26 -17
  33. package/scripts/harness/config-foundation/application/dto/resolved-config-output.ts +1 -0
  34. package/scripts/harness/config-foundation/application/usecases/load-resolved-config-use-case.ts +34 -2
  35. package/scripts/harness/config-foundation/application/usecases/migrate-schema-use-case.ts +89 -0
  36. package/scripts/harness/config-foundation/composition-root.ts +8 -0
  37. package/scripts/harness/config-foundation/domain/harness-config.ts +6 -0
  38. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +257 -0
  39. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +66 -0
  40. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -0
  41. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +546 -0
  42. package/scripts/harness/config-foundation/infrastructure/validators/ajv-config-schema-validator.ts +18 -6
  43. package/scripts/harness/config-foundation/presentation/cli/migrate-schema-command-handler.ts +83 -0
  44. package/scripts/harness/integrations/pre-commit.ts +211 -52
  45. package/scripts/harness/main.ts +460 -340
  46. package/scripts/harness/phase-dependency-model/domain/ports/story-reflection-file-system-port.ts +2 -4
  47. package/scripts/harness/phase-dependency-model/domain/services/story-reflection-checker.ts +54 -17
  48. package/scripts/harness/phase-dependency-model/infrastructure/filesystem/file-system-story-reflection-adapter.ts +154 -36
  49. package/scripts/harness/setup/skill-deployer.ts +140 -102
  50. package/scripts/harness/skill-quality/domain/errors/skill-quality-error.ts +24 -23
  51. package/scripts/harness/skill-quality/domain/value-objects/commit-message.ts +23 -7
  52. package/scripts/harness/traceability-model/application/usecases/apply-work-item-migration-usecase.ts +52 -0
  53. package/scripts/harness/traceability-model/application/usecases/plan-work-item-migration-usecase.ts +29 -0
  54. package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +83 -18
  55. package/scripts/harness/traceability-model/composition-root.ts +48 -30
  56. package/scripts/harness/traceability-model/domain/ports/design-document-port.ts +9 -15
  57. package/scripts/harness/traceability-model/domain/ports/work-item-migration-apply-port.ts +11 -0
  58. package/scripts/harness/traceability-model/domain/ports/work-item-migration-source-port.ts +9 -0
  59. package/scripts/harness/traceability-model/domain/services/work-item-migration-planner.ts +162 -0
  60. package/scripts/harness/traceability-model/domain/value-objects/work-item-frontmatter.ts +57 -0
  61. package/scripts/harness/traceability-model/domain/value-objects/work-item-migration-candidate.ts +47 -0
  62. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-apply-gateway.ts +110 -0
  63. package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-migration-source-gateway.ts +182 -0
  64. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +29 -43
  65. package/scripts/harness/traceability-model/infrastructure/parsers/work-item-frontmatter-parser.ts +136 -0
  66. package/scripts/harness/traceability-model/presentation/cli/migrate-work-items-command-handler.ts +186 -0
  67. package/skills/quick-implementor/SKILL.md +17 -1
  68. package/templates/.husky/commit-msg +1 -0
@@ -3,10 +3,8 @@
3
3
 
4
4
  export interface StoryReflectionFileSystemPort {
5
5
  listStoryDirectories(unitId: string): Promise<readonly string[]>;
6
+ storyAffectsUnit(storyId: string, unitId: string): Promise<boolean>;
6
7
  fileExists(path: string): Promise<boolean>;
7
8
  /** product 文書内に @story-id {storyId} アノテーションが含まれているか */
8
- fileContainsStoryAnnotation(
9
- productPath: string,
10
- storyId: string,
11
- ): Promise<boolean>;
9
+ fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean>;
12
10
  }
@@ -3,20 +3,22 @@
3
3
  * @unit phase-dependency-model
4
4
  */
5
5
 
6
- import type { StoryReflectionConfig } from '../values/story-reflection-config.js';
7
- import type { StoryReflectionFileSystemPort } from '../ports/story-reflection-file-system-port.js';
8
- import {
9
- StoryReflectionResult,
10
- type StoryReflectionViolation,
11
- } from '../values/story-reflection-result.js';
6
+ import type { StoryReflectionFileSystemPort } from "../ports/story-reflection-file-system-port.js";
7
+ import type { StoryReflectionConfig } from "../values/story-reflection-config.js";
8
+ import type { StoryReflectionMapping } from "../values/story-reflection-mapping.js";
9
+ import { StoryReflectionResult, type StoryReflectionViolation } from "../values/story-reflection-result.js";
10
+
11
+ const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
12
+
13
+ interface ResolvedInceptionPath {
14
+ readonly path: string;
15
+ readonly isCrossWorkItem: boolean;
16
+ }
12
17
 
13
18
  export class StoryReflectionChecker {
14
19
  constructor(private readonly fsPort: StoryReflectionFileSystemPort) {}
15
20
 
16
- async check(
17
- unitId: string,
18
- config: StoryReflectionConfig,
19
- ): Promise<StoryReflectionResult> {
21
+ async check(unitId: string, config: StoryReflectionConfig): Promise<StoryReflectionResult> {
20
22
  if (!config.enabled) {
21
23
  return StoryReflectionResult.pass();
22
24
  }
@@ -32,23 +34,28 @@ export class StoryReflectionChecker {
32
34
 
33
35
  for (const storyId of storyIds) {
34
36
  for (const mapping of config.mappings) {
35
- const { inception: inceptionPath, product: productPath } =
36
- mapping.resolve({ unitId, storyId });
37
+ const { product: productPath } = mapping.resolve({ unitId, storyId });
38
+ const resolvedInception = await this.resolveInceptionPath({
39
+ mapping,
40
+ unitId,
41
+ storyId,
42
+ });
37
43
 
38
- const inceptionExists = await this.fsPort.fileExists(inceptionPath);
44
+ if (resolvedInception === null) {
45
+ continue;
46
+ }
39
47
 
40
- if (!inceptionExists) {
48
+ if (resolvedInception.isCrossWorkItem && !(await this.fsPort.storyAffectsUnit(storyId, unitId))) {
41
49
  continue;
42
50
  }
43
51
 
44
- const hasAnnotation =
45
- await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
52
+ const hasAnnotation = await this.fsPort.fileContainsStoryAnnotation(productPath, storyId);
46
53
 
47
54
  if (!hasAnnotation) {
48
55
  const violation: StoryReflectionViolation = {
49
56
  storyId,
50
57
  mapping,
51
- inceptionPath,
58
+ inceptionPath: resolvedInception.path,
52
59
  productPath,
53
60
  };
54
61
 
@@ -63,4 +70,34 @@ export class StoryReflectionChecker {
63
70
 
64
71
  return StoryReflectionResult.create({ violations, warnings });
65
72
  }
73
+
74
+ private async resolveInceptionPath(input: {
75
+ readonly mapping: StoryReflectionMapping;
76
+ readonly unitId: string;
77
+ readonly storyId: string;
78
+ }): Promise<ResolvedInceptionPath | null> {
79
+ const normalPath = input.mapping.resolve({
80
+ unitId: input.unitId,
81
+ storyId: input.storyId,
82
+ }).inception;
83
+
84
+ if (await this.fsPort.fileExists(normalPath)) {
85
+ return { path: normalPath, isCrossWorkItem: false };
86
+ }
87
+
88
+ if (!CROSS_WORK_ITEM_PATTERN.test(input.storyId)) {
89
+ return null;
90
+ }
91
+
92
+ const crossPath = input.mapping.resolve({
93
+ unitId: "_cross",
94
+ storyId: input.storyId,
95
+ }).inception;
96
+
97
+ if (await this.fsPort.fileExists(crossPath)) {
98
+ return { path: crossPath, isCrossWorkItem: true };
99
+ }
100
+
101
+ return null;
102
+ }
66
103
  }
@@ -3,9 +3,15 @@
3
3
  * @unit phase-dependency-model
4
4
  */
5
5
 
6
- import { readdir, readFile, access } from 'node:fs/promises';
7
- import path from 'node:path';
8
- import type { StoryReflectionFileSystemPort } from '../../domain/ports/story-reflection-file-system-port.js';
6
+ import type { Dirent } from "node:fs";
7
+ import { access, readdir, readFile } from "node:fs/promises";
8
+ import path from "node:path";
9
+ import type { StoryReflectionFileSystemPort } from "../../domain/ports/story-reflection-file-system-port.js";
10
+
11
+ const CROSS_WORK_ITEM_PATTERN = /^WI-\d+$/;
12
+ const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
13
+ const AFFECTS_INLINE_PATTERN = /^affects:\s*\[([^\]]*)\]\s*$/m;
14
+ const LEGACY_ID_PATTERN = /^legacy_id:\s*([A-Z][\w]+-\d+)\s*$/m;
9
15
 
10
16
  export interface FileSystemStoryReflectionAdapterDeps {
11
17
  readonly rootDir: string;
@@ -15,39 +21,67 @@ export interface FileSystemStoryReflectionAdapterDeps {
15
21
 
16
22
  /**
17
23
  * ファイルシステム上の docs/inception ディレクトリ列挙と
18
- * product 文書内 `@story-id` アノテーション検索を実装するアダプタ。
24
+ * product 文書内 `@story-id` / `@issue-id` / `@work-item-id` アノテーション
25
+ * 検索を実装するアダプタ。
19
26
  *
20
27
  * 存在しないパス・読めないファイルは true/false に正規化し例外を投げない
21
28
  * ことで、上位 (StoryReflectionChecker) の純粋性を保つ。
29
+ *
30
+ * ISSUE-026 / H02-04: `@issue-id` / `@work-item-id` も `@story-id` と同等に
31
+ * 認識する(WI 一本化への段階移行のため、既存 `@story-id` と併存)。
22
32
  */
23
- export class FileSystemStoryReflectionAdapter
24
- implements StoryReflectionFileSystemPort
25
- {
33
+ export class FileSystemStoryReflectionAdapter implements StoryReflectionFileSystemPort {
26
34
  private readonly rootDir: string;
27
35
  private readonly inceptionRoot: string;
28
36
 
29
37
  constructor(deps: FileSystemStoryReflectionAdapterDeps) {
30
38
  this.rootDir = deps.rootDir;
31
- this.inceptionRoot = deps.inceptionRoot ?? 'docs/inception';
39
+ this.inceptionRoot = deps.inceptionRoot ?? "docs/inception";
32
40
  }
33
41
 
34
42
  async listStoryDirectories(unitId: string): Promise<readonly string[]> {
35
- const unitDir = path.join(this.rootDir, this.inceptionRoot, unitId);
43
+ const ids = new Set<string>();
36
44
 
37
- let entries;
38
- try {
39
- entries = await readdir(unitDir, { withFileTypes: true });
40
- } catch {
41
- return [];
45
+ for (const id of await this.listUnitWorkItemDirectories(unitId)) {
46
+ ids.add(id);
47
+ }
48
+ for (const id of await this.listCrossWorkItemDirectories()) {
49
+ ids.add(id);
42
50
  }
43
51
 
52
+ return [...ids].sort();
53
+ }
54
+
55
+ private async listUnitWorkItemDirectories(unitId: string): Promise<readonly string[]> {
56
+ const unitDir = path.join(this.rootDir, this.inceptionRoot, unitId);
57
+ const entries = await this.readDirectories(unitDir);
58
+
59
+ return entries
60
+ .filter((entry) => entry.isDirectory())
61
+ .map((entry) => entry.name)
62
+ .filter((name) => !name.startsWith("_") && !name.startsWith("."))
63
+ .sort();
64
+ }
65
+
66
+ private async listCrossWorkItemDirectories(): Promise<readonly string[]> {
67
+ const crossDir = path.join(this.rootDir, this.inceptionRoot, "_cross");
68
+ const entries = await this.readDirectories(crossDir);
69
+
44
70
  return entries
45
71
  .filter((entry) => entry.isDirectory())
46
72
  .map((entry) => entry.name)
47
- .filter((name) => !name.startsWith('_') && !name.startsWith('.'))
73
+ .filter((name) => CROSS_WORK_ITEM_PATTERN.test(name))
48
74
  .sort();
49
75
  }
50
76
 
77
+ private async readDirectories(dir: string): Promise<readonly Dirent[]> {
78
+ try {
79
+ return await readdir(dir, { withFileTypes: true });
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
51
85
  async fileExists(relativePath: string): Promise<boolean> {
52
86
  const absolutePath = path.join(this.rootDir, relativePath);
53
87
  try {
@@ -58,37 +92,121 @@ export class FileSystemStoryReflectionAdapter
58
92
  }
59
93
  }
60
94
 
61
- async fileContainsStoryAnnotation(
62
- productPath: string,
63
- storyId: string,
64
- ): Promise<boolean> {
95
+ async storyAffectsUnit(storyId: string, unitId: string): Promise<boolean> {
96
+ if (!CROSS_WORK_ITEM_PATTERN.test(storyId)) {
97
+ return true;
98
+ }
99
+
100
+ let content: string;
101
+ try {
102
+ content = await readFile(
103
+ path.join(this.rootDir, this.inceptionRoot, "_cross", storyId, "description.md"),
104
+ "utf8",
105
+ );
106
+ } catch {
107
+ return true;
108
+ }
109
+
110
+ const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
111
+ if (frontmatter === undefined) {
112
+ return true;
113
+ }
114
+
115
+ const affects = AFFECTS_INLINE_PATTERN.exec(frontmatter)?.[1]
116
+ .split(",")
117
+ .map((value) => value.trim())
118
+ .filter((value) => value.length > 0);
119
+
120
+ if (affects === undefined) {
121
+ return true;
122
+ }
123
+
124
+ return affects.includes(unitId);
125
+ }
126
+
127
+ async fileContainsStoryAnnotation(productPath: string, storyId: string): Promise<boolean> {
65
128
  const absolutePath = path.join(this.rootDir, productPath);
66
129
 
67
130
  let content: string;
68
131
  try {
69
- content = await readFile(absolutePath, 'utf8');
132
+ content = await readFile(absolutePath, "utf8");
70
133
  } catch {
71
134
  return false;
72
135
  }
73
136
 
74
- // `@story-id` の直後にカンマ/空白区切りで列挙される story ID を抽出。
75
- // 例: `@story-id US-001`, `@story-id US-001, US-002`,
76
- // `<!-- @story-id US-001 -->`
77
- // 行末または `-->` まで(story ID 自体の `-` は許容するため `-->` を明示終端に使う)。
78
- const pattern = /@story-id[ \t]+([^\n\r]+)/g;
79
- let match: RegExpExecArray | null;
80
- while ((match = pattern.exec(content)) !== null) {
137
+ // `@story-id` / `@issue-id` / `@work-item-id` 直後にカンマ/空白区切りで
138
+ // 列挙される ID を抽出。
139
+ // 例: `@story-id US-001`, `@work-item-id WI-001, WI-002`,
140
+ // `<!-- @issue-id ISSUE-026 -->`
141
+ // capture[1] がアノテーション種別(未使用・将来拡張用)、capture[2] が ID 本体。
142
+ // 行末または `-->` まで(ID 自体の `-` を許容するため `-->` を明示終端に使う)。
143
+ const annotationIds = this.extractAnnotationIds(content);
144
+ if (annotationIds.includes(storyId)) {
145
+ return true;
146
+ }
147
+
148
+ const legacyId = await this.readLegacyId(storyId);
149
+ return legacyId !== null && annotationIds.includes(legacyId);
150
+ }
151
+
152
+ private extractAnnotationIds(content: string): readonly string[] {
153
+ const ids: string[] = [];
154
+
155
+ const annotationPattern = /@(story-id|issue-id|work-item-id)[ \t]+([^\n\r]+)/g;
156
+ let match = annotationPattern.exec(content);
157
+ while (match !== null) {
81
158
  // `-->` 以降の HTML コメント閉じ記号を取り除く
82
- const raw = match[1].replace(/-->.*$/, '');
83
- const ids = raw
84
- .split(/[\s,]+/)
85
- .map((s) => s.trim())
86
- .filter((s) => s.length > 0);
87
- if (ids.includes(storyId)) {
88
- return true;
89
- }
159
+ const raw = match[2].replace(/-->.*$/, "");
160
+ ids.push(
161
+ ...raw
162
+ .split(/[\s,]+/)
163
+ .map((s) => s.trim())
164
+ .filter((s) => s.length > 0),
165
+ );
166
+ match = annotationPattern.exec(content);
90
167
  }
91
168
 
92
- return false;
169
+ return ids;
170
+ }
171
+
172
+ private async readLegacyId(storyId: string): Promise<string | null> {
173
+ if (!CROSS_WORK_ITEM_PATTERN.test(storyId)) {
174
+ return null;
175
+ }
176
+
177
+ for (const candidate of await this.listDescriptionCandidates(storyId)) {
178
+ const legacyId = await this.tryReadLegacyId(candidate);
179
+ if (legacyId !== null) return legacyId;
180
+ }
181
+
182
+ return null;
183
+ }
184
+
185
+ private async listDescriptionCandidates(storyId: string): Promise<readonly string[]> {
186
+ const inceptionDir = path.join(this.rootDir, this.inceptionRoot);
187
+ const candidates: string[] = [path.join(inceptionDir, "_cross", storyId, "description.md")];
188
+
189
+ for (const entry of await this.readDirectories(inceptionDir)) {
190
+ if (!entry.isDirectory()) continue;
191
+ const name = entry.name;
192
+ if (name.startsWith("_") || name.startsWith(".") || name === "issues") continue;
193
+ candidates.push(path.join(inceptionDir, name, storyId, "description.md"));
194
+ }
195
+
196
+ return candidates;
197
+ }
198
+
199
+ private async tryReadLegacyId(absolutePath: string): Promise<string | null> {
200
+ let content: string;
201
+ try {
202
+ content = await readFile(absolutePath, "utf8");
203
+ } catch {
204
+ return null;
205
+ }
206
+
207
+ const frontmatter = FRONTMATTER_PATTERN.exec(content)?.[1];
208
+ if (frontmatter === undefined) return null;
209
+
210
+ return LEGACY_ID_PATTERN.exec(frontmatter)?.[1] ?? null;
93
211
  }
94
212
  }