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
@@ -13,33 +13,108 @@
13
13
  * 2 = runtime error
14
14
  */
15
15
 
16
- import { execSync } from 'node:child_process';
17
- import { createValidatorSystemModule } from '../validator-system/composition-root.js';
18
- import { HumanValidationResultFormatter } from '../validator-system/presentation/formatters/human-validation-result-formatter.js';
19
- import type { AggregatedValidationReport } from '../validator-system/application/dto/aggregated-validation-report.js';
20
- import type { ValidationResultContract } from '../validator-system/application/dto/validation-result-contract.js';
21
- import { createTraceabilityModelModule } from '../traceability-model/composition-root.js';
22
- import type { ValidateMetadataCommandOutput } from '../traceability-model/presentation/cli/validate-metadata-command-handler.js';
23
-
24
- const GREEN = '\x1b[32m';
25
- const RED = '\x1b[31m';
26
- const BOLD = '\x1b[1m';
27
- const DIM = '\x1b[2m';
28
- const RESET = '\x1b[0m';
29
-
30
- const TS_EXTENSION = '.ts';
31
- const MD_EXTENSION = '.md';
32
- const TEST_FILE_SUFFIXES = Object.freeze([
33
- '.test.ts',
34
- '.test.tsx',
35
- '.spec.ts',
36
- '.spec.tsx',
37
- ]);
16
+ import { execSync } from "node:child_process";
17
+ import { readFile } from "node:fs/promises";
18
+ import { createTraceabilityModelModule } from "../traceability-model/composition-root.js";
19
+ import type { ValidateMetadataCommandOutput } from "../traceability-model/presentation/cli/validate-metadata-command-handler.js";
20
+ import type { AggregatedValidationReport } from "../validator-system/application/dto/aggregated-validation-report.js";
21
+ import type { ValidationResultContract } from "../validator-system/application/dto/validation-result-contract.js";
22
+ import { createValidatorSystemModule } from "../validator-system/composition-root.js";
23
+ import { HumanValidationResultFormatter } from "../validator-system/presentation/formatters/human-validation-result-formatter.js";
24
+
25
+ const GREEN = "\x1b[32m";
26
+ const RED = "\x1b[31m";
27
+ const BOLD = "\x1b[1m";
28
+ const DIM = "\x1b[2m";
29
+ const RESET = "\x1b[0m";
30
+
31
+ const TS_EXTENSION = ".ts";
32
+ const MD_EXTENSION = ".md";
33
+ const WORK_ITEM_PATH_PATTERN = /(?:^|\/)WI-\d+(?:\/|$)/;
34
+ const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*WI-\d+\s*$/m;
35
+ const TEST_FILE_SUFFIXES = Object.freeze([".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"]);
38
36
 
39
37
  function isTestFile(path: string): boolean {
40
38
  return TEST_FILE_SUFFIXES.some((suffix) => path.endsWith(suffix));
41
39
  }
42
40
 
41
+ function isMetadataMarkdownFile(path: string): boolean {
42
+ return path.startsWith("docs/inception/") || path.startsWith("docs/product/");
43
+ }
44
+
45
+ const HARNESS_ROOT_PREFIX = "scripts/harness/";
46
+ const TESTS_SEGMENT = "__tests__";
47
+ const UNIT_ANNOTATION_PATTERN = /^\s*(?:\/\/|\*)\s*@unit\s+(\S+)/m;
48
+
49
+ /**
50
+ * staged TS file path から所属 Unit 名を導出する。
51
+ * scripts/harness/{unit}/... → unit
52
+ * scripts/harness/__tests__/{unit|integration}/{unit}/...
53
+ * → unit
54
+ * 上記パターンに合致しないパス(例: scripts/harness/integrations/pre-commit.ts)は
55
+ * undefined を返し、呼び出し側が file 内 `@unit` コメントにフォールバックする。
56
+ */
57
+ function deriveUnitNameFromPath(filePath: string): string | undefined {
58
+ if (!filePath.startsWith(HARNESS_ROOT_PREFIX)) return undefined;
59
+ const parts = filePath.slice(HARNESS_ROOT_PREFIX.length).split("/");
60
+ if (parts.length < 2) return undefined;
61
+ if (parts[0] === TESTS_SEGMENT) {
62
+ return parts.length >= 3 ? parts[2] : undefined;
63
+ }
64
+ return parts[0];
65
+ }
66
+
67
+ /**
68
+ * ファイル本体から `// @unit <name>` / `* @unit <name>` を抽出する。
69
+ * 読み込み失敗や annotation 不在時は undefined。
70
+ */
71
+ async function deriveUnitNameFromFile(filePath: string): Promise<string | undefined> {
72
+ try {
73
+ const content = await readFile(filePath, "utf-8");
74
+ const match = UNIT_ANNOTATION_PATTERN.exec(content);
75
+ return match?.[1];
76
+ } catch {
77
+ return undefined;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * path-based 推定と file-based 推定を組み合わせて Unit 名を解決する。
83
+ * `@unit` アノテーションが存在する場合はそれを正として採用し、
84
+ * 無い場合のみ path-based 推定にフォールバックする(cross-cutting dir 対応)。
85
+ */
86
+ async function resolveUnitName(filePath: string): Promise<string | undefined> {
87
+ const fileUnit = await deriveUnitNameFromFile(filePath);
88
+ if (fileUnit) return fileUnit;
89
+ return deriveUnitNameFromPath(filePath);
90
+ }
91
+
92
+ /**
93
+ * 複数 Unit の L2 validator 実行結果を、ValidatorId 単位で集約する。
94
+ * 同じ ValidatorId に対して、どの Unit でも 1 件でも fail / skip があれば
95
+ * fail / skip を優先(厳しい側を採用)。
96
+ */
97
+ function mergePerUnitResults(
98
+ runs: readonly (readonly ValidationResultContract[])[],
99
+ ): readonly ValidationResultContract[] {
100
+ const byId = new Map<string, ValidationResultContract>();
101
+ for (const run of runs) {
102
+ for (const result of run) {
103
+ const existing = byId.get(result.validatorId);
104
+ if (!existing) {
105
+ byId.set(result.validatorId, result);
106
+ continue;
107
+ }
108
+ const existingFailed = !existing.passed && !existing.skipped;
109
+ const incomingFailed = !result.passed && !result.skipped;
110
+ if (incomingFailed && !existingFailed) {
111
+ byId.set(result.validatorId, result);
112
+ }
113
+ }
114
+ }
115
+ return [...byId.values()];
116
+ }
117
+
43
118
  interface RunL2Input {
44
119
  readonly targetPaths: readonly string[];
45
120
  readonly unitName: string;
@@ -69,14 +144,23 @@ export interface PreCommitResult {
69
144
  readonly stdout: string;
70
145
  }
71
146
 
147
+ export interface PreCommitOptions {
148
+ /**
149
+ * Optional commit message supplied by CI / commit-msg style callers.
150
+ * Native pre-commit hooks run before Git creates the message, so this is
151
+ * intentionally opt-in and preserves existing local pre-commit behavior.
152
+ */
153
+ readonly commitMessage?: string;
154
+ }
155
+
72
156
  function getStagedFiles(): string[] {
73
157
  try {
74
- const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
75
- encoding: 'utf-8',
76
- stdio: ['ignore', 'pipe', 'ignore'],
158
+ const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
159
+ encoding: "utf-8",
160
+ stdio: ["ignore", "pipe", "ignore"],
77
161
  });
78
162
  return output
79
- .split('\n')
163
+ .split("\n")
80
164
  .map((f) => f.trim())
81
165
  .filter((f) => f.length > 0);
82
166
  } catch {
@@ -84,15 +168,13 @@ function getStagedFiles(): string[] {
84
168
  }
85
169
  }
86
170
 
87
- function buildReport(
88
- results: readonly ValidationResultContract[],
89
- ): AggregatedValidationReport {
171
+ function buildReport(results: readonly ValidationResultContract[]): AggregatedValidationReport {
90
172
  const passed = results.filter((r) => r.passed && !r.skipped).length;
91
173
  const failed = results.filter((r) => !r.passed && !r.skipped).length;
92
174
  const skipped = results.filter((r) => r.skipped).length;
93
175
  const allErrors = results.flatMap((r) => r.errors);
94
- const errorCount = allErrors.filter((e) => e.severity === 'error').length;
95
- const warnCount = allErrors.filter((e) => e.severity === 'warning').length;
176
+ const errorCount = allErrors.filter((e) => e.severity === "error").length;
177
+ const warnCount = allErrors.filter((e) => e.severity === "warning").length;
96
178
  return {
97
179
  overallPassed: failed === 0,
98
180
  totalValidators: results.length,
@@ -110,15 +192,26 @@ function buildReport(
110
192
  }
111
193
 
112
194
  function maxExitCode(a: 0 | 1 | 2, b: 0 | 1 | 2): 0 | 1 | 2 {
113
- return (Math.max(a, b) as 0 | 1 | 2);
195
+ return Math.max(a, b) as 0 | 1 | 2;
196
+ }
197
+
198
+ function requiresWorkItemTrailer(stagedFiles: readonly string[]): boolean {
199
+ return stagedFiles.some(
200
+ (filePath) => filePath.startsWith("docs/inception/") && WORK_ITEM_PATH_PATTERN.test(filePath),
201
+ );
202
+ }
203
+
204
+ function hasWorkItemTrailer(commitMessage: string): boolean {
205
+ return WORK_ITEM_TRAILER_PATTERN.test(commitMessage);
114
206
  }
115
207
 
116
208
  export async function runPreCommit(
117
209
  stagedFiles: readonly string[],
118
210
  deps: PreCommitDeps,
211
+ options: PreCommitOptions = {},
119
212
  ): Promise<PreCommitResult> {
120
213
  const tsFiles = stagedFiles.filter((f) => f.endsWith(TS_EXTENSION));
121
- const mdFiles = stagedFiles.filter((f) => f.endsWith(MD_EXTENSION));
214
+ const mdFiles = stagedFiles.filter((f) => f.endsWith(MD_EXTENSION) && isMetadataMarkdownFile(f));
122
215
  const testFiles = tsFiles.filter((f) => isTestFile(f));
123
216
  const metadataFiles = [...mdFiles, ...testFiles];
124
217
 
@@ -131,20 +224,39 @@ export async function runPreCommit(
131
224
 
132
225
  const sections: string[] = [];
133
226
  sections.push(
134
- `${BOLD}[phasegate]${RESET} Pre-commit check ` +
135
- `(${tsFiles.length} .ts file(s), ${mdFiles.length} .md file(s))`,
227
+ `${BOLD}[phasegate]${RESET} Pre-commit check ` + `(${tsFiles.length} .ts file(s), ${mdFiles.length} .md file(s))`,
136
228
  );
137
229
 
138
230
  let exitCode: 0 | 1 | 2 = 0;
139
231
 
140
232
  if (tsFiles.length > 0) {
141
- const results = await deps.runL2ValidatorsUseCase.execute({
142
- targetPaths: tsFiles,
143
- unitName: '',
144
- currentPhase: '',
145
- });
146
- const report = buildReport(results);
147
- sections.push('');
233
+ // staged TS file を Unit ごとにグルーピングし、Unit 単位で L2 phase gate
234
+ // (L2-001 の `{unit}_unit.md` 等)を評価する。Unit を特定できないファイルは
235
+ // 別グループ(unitName='')として従来挙動で評価する。
236
+ const filesByUnit = new Map<string, string[]>();
237
+ for (const f of tsFiles) {
238
+ const unit = (await resolveUnitName(f)) ?? "";
239
+ const bucket = filesByUnit.get(unit);
240
+ if (bucket) {
241
+ bucket.push(f);
242
+ } else {
243
+ filesByUnit.set(unit, [f]);
244
+ }
245
+ }
246
+
247
+ const runs: (readonly ValidationResultContract[])[] = [];
248
+ for (const [unitName, unitFiles] of filesByUnit) {
249
+ const results = await deps.runL2ValidatorsUseCase.execute({
250
+ targetPaths: unitFiles,
251
+ unitName,
252
+ currentPhase: "",
253
+ });
254
+ runs.push(results);
255
+ }
256
+
257
+ const merged = mergePerUnitResults(runs);
258
+ const report = buildReport(merged);
259
+ sections.push("");
148
260
  sections.push(`${BOLD}== TypeScript 実装 (${tsFiles.length} file(s)) ==${RESET}`);
149
261
  sections.push(new HumanValidationResultFormatter().format(report));
150
262
  if (!report.overallPassed) {
@@ -156,15 +268,26 @@ export async function runPreCommit(
156
268
  const metadataResult = await deps.validateMetadataCommandHandler.execute({
157
269
  filePaths: metadataFiles,
158
270
  });
159
- sections.push('');
160
- sections.push(
161
- `${BOLD}== 設計 / テスト メタデータ注釈 (${metadataFiles.length} file(s)) ==${RESET}`,
162
- );
271
+ sections.push("");
272
+ sections.push(`${BOLD}== 設計 / テスト メタデータ注釈 (${metadataFiles.length} file(s)) ==${RESET}`);
163
273
  sections.push(metadataResult.text);
164
274
  exitCode = maxExitCode(exitCode, metadataResult.exitCode);
165
275
  }
166
276
 
167
- sections.push('');
277
+ if (options.commitMessage !== undefined && requiresWorkItemTrailer(stagedFiles)) {
278
+ sections.push("");
279
+ sections.push(`${BOLD}== Work-Item trailer ==${RESET}`);
280
+ if (hasWorkItemTrailer(options.commitMessage)) {
281
+ sections.push(`${GREEN}PASS${RESET} Work-Item trailer is present.`);
282
+ } else {
283
+ sections.push(
284
+ `${RED}FAIL${RESET} Commit message must include \`Work-Item: WI-XXX\` when WI documents are staged.`,
285
+ );
286
+ exitCode = maxExitCode(exitCode, 1);
287
+ }
288
+ }
289
+
290
+ sections.push("");
168
291
  if (exitCode === 0) {
169
292
  sections.push(`${GREEN}[phasegate]${RESET} All checks passed.`);
170
293
  } else {
@@ -173,7 +296,7 @@ export async function runPreCommit(
173
296
 
174
297
  return {
175
298
  exitCode,
176
- stdout: sections.join('\n'),
299
+ stdout: sections.join("\n"),
177
300
  };
178
301
  }
179
302
 
@@ -183,10 +306,46 @@ export async function runPreCommitCli(): Promise<void> {
183
306
  const validatorMod = createValidatorSystemModule();
184
307
  const traceabilityMod = createTraceabilityModelModule(process.cwd());
185
308
 
186
- const result = await runPreCommit(stagedFiles, {
187
- runL2ValidatorsUseCase: validatorMod.runL2ValidatorsUseCase,
188
- validateMetadataCommandHandler: traceabilityMod.validateMetadataCommandHandler,
189
- });
309
+ const result = await runPreCommit(
310
+ stagedFiles,
311
+ {
312
+ runL2ValidatorsUseCase: validatorMod.runL2ValidatorsUseCase,
313
+ validateMetadataCommandHandler: traceabilityMod.validateMetadataCommandHandler,
314
+ },
315
+ {
316
+ commitMessage: process.env.PHASEGATE_COMMIT_MESSAGE,
317
+ },
318
+ );
319
+
320
+ process.stdout.write(`${result.stdout}\n`);
321
+ process.exit(result.exitCode);
322
+ } catch (err) {
323
+ const msg = err instanceof Error ? err.message : String(err);
324
+ process.stderr.write(`${RED}[phasegate] Unexpected error:${RESET} ${msg}\n`);
325
+ process.exit(2);
326
+ }
327
+ }
328
+
329
+ export async function runCommitMsgCli(commitMessagePath: string | undefined): Promise<void> {
330
+ try {
331
+ if (!commitMessagePath) {
332
+ process.stderr.write(`${RED}[phasegate] commit-msg requires a commit message file path.${RESET}\n`);
333
+ process.exit(2);
334
+ }
335
+
336
+ const stagedFiles = getStagedFiles();
337
+ const commitMessage = await readFile(commitMessagePath, "utf-8");
338
+ const validatorMod = createValidatorSystemModule();
339
+ const traceabilityMod = createTraceabilityModelModule(process.cwd());
340
+
341
+ const result = await runPreCommit(
342
+ stagedFiles,
343
+ {
344
+ runL2ValidatorsUseCase: validatorMod.runL2ValidatorsUseCase,
345
+ validateMetadataCommandHandler: traceabilityMod.validateMetadataCommandHandler,
346
+ },
347
+ { commitMessage },
348
+ );
190
349
 
191
350
  process.stdout.write(`${result.stdout}\n`);
192
351
  process.exit(result.exitCode);