phasegate 0.128.0 → 0.130.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 (20) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +1 -1
  3. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +19 -0
  4. package/scripts/harness/config-foundation/index.ts +2 -1
  5. package/scripts/harness/harness-api/infrastructure/adapters/nyquist-validation-impact-analysis-adapter.ts +7 -1
  6. package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +6 -4
  7. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +19 -3
  8. package/scripts/harness/integrations/pre-commit.ts +38 -4
  9. package/scripts/harness/main.ts +15 -23
  10. package/scripts/harness/phase-dependency-model/domain/definitions/full-phase-nodes.ts +5 -5
  11. package/scripts/harness/phase-dependency-model/domain/definitions/minimal-phase-nodes.ts +1 -1
  12. package/scripts/harness/phase-dependency-model/domain/definitions/standard-phase-nodes.ts +2 -2
  13. package/scripts/harness/phase-dependency-model/domain/values/artifact.ts +9 -4
  14. package/scripts/harness/traceability-model/composition-root.ts +33 -5
  15. package/scripts/harness/traceability-model/domain/services/traceability-chain-builder.ts +9 -4
  16. package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +2 -4
  17. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +6 -2
  18. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-story-catalog-gateway.ts +8 -6
  19. package/scripts/harness/traceability-model/infrastructure/gateways/markdown-unit-definition-gateway.ts +8 -2
  20. package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.130.0] - 2026-05-08
11
+
12
+ ### Fixed
13
+
14
+ - **WI-093 — `paths.designDocs` を L2-001 / traceability-model の product 直下文書まで完全 threading (GitHub Issue #4 finding #4)** — WI-085 で残っていた `docs/product/product_overview.md` / `docs/product/user_stories.md` hardcoded 経路を解消。
15
+ - **phase-dependency-model**: Level 1 product 文書を `{designDocsRoot}/../...` で定義し、`Artifact.resolve()` / `expandRoots()` で POSIX 正規化。`paths.designDocs: "mydocs/product/construction"` の場合、L2 blocker は `mydocs/product/product_overview.md` / `mydocs/product/user_stories.md` を参照する。
16
+ - **traceability-model**: `MarkdownStoryCatalogGateway` / `MarkdownDesignDocumentGateway` / `MarkdownUnitDefinitionGateway` / `TraceabilityChainBuilder` に custom design docs root を注入し、`mydocs/product/user_stories.md` と `mydocs/product/construction/{unit}` を読むよう修正。
17
+ - **後方互換**: config 未指定時は従来通り `docs/product/user_stories.md` / `docs/product/construction/{unit}` を利用。
18
+ - **検証 (publish 前 local/self-host)**: `pnpm exec tsc --noEmit` pass。対象テスト 4 files / 29 tests pass。`pnpm test` は 453 files / 3520 tests pass。`/private/tmp/phasegate-local-wi093` の symlink なし reporter fixture で `validate --layer L2 --format human` を実行し、L2-001 blocker が `mydocs/product/product_overview.md` を参照することを確認。post-publish dogfood は publish 後に実施予定。
19
+
20
+ ## [0.129.0] - 2026-05-08
21
+
22
+ ### Fixed
23
+
24
+ - **WI-092 — `createValidatorSystemModule()` 残 5 site の config threading 漏れを sweep** — v0.128.0 で `validate --layer L4` 経路のみ修正した DI 配線漏れの follow-up として、harness-api / pre-commit 経路でも user config (`layers.L2/L3/L4.enabled`) が validator-system composition root に渡るよう修正。
25
+ - **対象**: `ValidatorSystemExecutionAdapter` の `runL3Validators()` / `runAllValidators()` / `runDriftDetection()`、および `runPreCommitCli()` / `runCommitMsgCli()` の計 5 site。
26
+ - **共通 translator 化**: `toValidatorSystemConfig()` を `config-foundation/application/mappers/validator-system-config-mapper.ts` に移動し、`main.ts` / harness-api adapter / pre-commit CLI から共通利用。渡す値は `project.preset` と `layers.L2/L3/L4.enabled` のみに限定し、`validators` 配列は渡さない。validator catalog は validator-system の default 解決に委ねる。
27
+ - **fallback**: `phasegate.config.json` 不在時のみ validator-system の default config にフォールバック。不正 config は従来通り runtime error として扱う。
28
+ - **検証 (publish 前 local/self-host)**: PhaseGate 自身の `phasegate.config.json` (`layers.L4.enabled: false`) で `validate --layer L4` は exit 0 / PASS / validator 0 件。`phasegate:detect-drift --json` は drift detection direct 経路を維持し、既存 drift 2065 件により exit 1。
29
+ - **テスト**: `pnpm test` は 452 files / 3518 tests pass。対象テスト 3 files / 11 tests pass。変更ファイルの `validate-metadata` pass。
30
+ - **post-publish dogfood**: `/private/tmp/phasegate-dogfood-wi092` で `npx phasegate@0.129.0` を使い検証済み。`validate --layer L4 --format human` は exit 0 / PASS / validator 0 件、`phasegate:status --json` は L4 `enabled:false`、`phasegate:detect-drift --json` は fixture の drift 1 件を返して exit 1、`pre-commit` は staged markdown 2 件を検査して PASS。
31
+
10
32
  ## [0.128.0] - 2026-05-08
11
33
 
12
34
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.128.0",
3
+ "version": "0.130.0",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @layer application
3
+ * @unit config-foundation
4
+ * @work-item-id WI-092
5
+ */
6
+ import type { HarnessConfigV2 } from '../../domain/harness-config.js';
7
+
8
+ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefined): object | undefined {
9
+ if (!resolvedConfig) return undefined;
10
+
11
+ return {
12
+ project: { preset: resolvedConfig.project.preset },
13
+ layers: {
14
+ L2: { enabled: resolvedConfig.layers.L2.enabled },
15
+ L3: { enabled: resolvedConfig.layers.L3.enabled },
16
+ L4: { enabled: resolvedConfig.layers.L4.enabled },
17
+ },
18
+ };
19
+ }
@@ -1,9 +1,10 @@
1
1
  // @unit config-foundation
2
- // @layer public-api
2
+ // @layer application
3
3
 
4
4
  export { createConfigFoundationModule } from './composition-root.js';
5
5
 
6
6
  export { LoadResolvedConfigUseCase } from './application/usecases/load-resolved-config-use-case.js';
7
+ export { toValidatorSystemConfig } from './application/mappers/validator-system-config-mapper.js';
7
8
 
8
9
  export type { ResolvedConfigOutput } from './application/dto/resolved-config-output.js';
9
10
  export type { FeatureToggleResult } from './application/dto/feature-toggle-result.js';
@@ -1,3 +1,4 @@
1
+ // @unit harness-api
1
2
  // @layer infrastructure
2
3
  // nyquist-validation-impact-analysis-adapter.ts — NyquistValidationImpactAnalysisAdapter
3
4
  // Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
@@ -24,8 +25,13 @@ export class NyquistValidationImpactAnalysisAdapter implements ImpactAnalysisPor
24
25
  return {
25
26
  async analyzeImpact(storyId: string): Promise<ImpactAnalysisResult | null> {
26
27
  try {
28
+ const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');
29
+ const configModule = createConfigFoundationModule();
30
+ const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
27
31
  const { createTraceabilityModelModule } = await import('../../../traceability-model/composition-root.js');
28
- const traceModule = createTraceabilityModelModule(rootDir);
32
+ const traceModule = createTraceabilityModelModule(rootDir, {
33
+ pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
34
+ });
29
35
  const storyIds = await traceModule.storyCatalog.getAllStoryIds();
30
36
 
31
37
  const { createNyquistValidationModule } = await import('../../../nyquist-validation/composition-root.js');
@@ -25,15 +25,17 @@ export class PhaseDependencyModelQueryAdapter implements PhaseGateQueryPort {
25
25
  return {
26
26
  async queryAllStories(): Promise<PhaseGateStoryResult[]> {
27
27
  try {
28
- const { createTraceabilityModelModule } = await import('../../../traceability-model/composition-root.js');
29
- const traceModule = createTraceabilityModelModule(rootDir);
30
- const storyIds = await traceModule.storyCatalog.getAllStoryIds();
31
-
32
28
  // WI-085: paths config を phase-dependency-model に流入させる
33
29
  const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');
34
30
  const { toPhaseConfigSection } = await import('../../../config-foundation/application/mappers/phase-config-section-mapper.js');
35
31
  const configModule = createConfigFoundationModule();
36
32
  const resolvedConfig = await configModule.usecases.loadResolvedConfigUseCase.execute();
33
+ const { createTraceabilityModelModule } = await import('../../../traceability-model/composition-root.js');
34
+ const traceModule = createTraceabilityModelModule(rootDir, {
35
+ pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs },
36
+ });
37
+ const storyIds = await traceModule.storyCatalog.getAllStoryIds();
38
+
37
39
  const { createPhaseDependencyModelModule } = await import('../../../phase-dependency-model/composition-root.js');
38
40
  const phaseModule = createPhaseDependencyModelModule({
39
41
  rootDir,
@@ -1,10 +1,14 @@
1
+ // @unit harness-api
1
2
  // @layer infrastructure
3
+ // @work-item-id WI-092
2
4
  // validator-system-execution-adapter.ts — ValidatorSystemExecutionAdapter
3
5
  // Wave 2完了後にリアル実装へ差し替え(旧: @stub: wave2-pending)
4
6
 
5
7
  import type { ValidatorExecutionPort } from '../../domain/ports/validator-execution-port.js';
6
8
  import type { ValidatorCheckItem } from '../../domain/value-objects/ci-check-result.js';
7
9
  import type { DriftItem } from '../../domain/value-objects/drift-report-summary.js';
10
+ import { toValidatorSystemConfig } from '../../../config-foundation/application/mappers/validator-system-config-mapper.js';
11
+ import { ConfigNotFoundError } from '../../../config-foundation/infrastructure/repositories/file-system-config-repository.js';
8
12
 
9
13
  // Override interface preserved for testing
10
14
  export interface IValidatorSystemStub {
@@ -21,10 +25,22 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
21
25
  }
22
26
 
23
27
  private static createRealImpl(): IValidatorSystemStub {
28
+ async function loadValidatorSystemConfig(): Promise<object | undefined> {
29
+ const { createConfigFoundationModule } = await import('../../../config-foundation/composition-root.js');
30
+ const configMod = createConfigFoundationModule();
31
+ try {
32
+ const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
33
+ return toValidatorSystemConfig(resolvedConfig.config);
34
+ } catch (err) {
35
+ if (err instanceof ConfigNotFoundError) return undefined;
36
+ throw err;
37
+ }
38
+ }
39
+
24
40
  return {
25
41
  async runL3Validators(): Promise<ValidatorCheckItem[]> {
26
42
  const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
27
- const mod = createValidatorSystemModule();
43
+ const mod = createValidatorSystemModule(await loadValidatorSystemConfig());
28
44
  const results = await mod.runL3ValidatorsUseCase.execute({ targetPaths: [] });
29
45
  return results.map((r) => ({
30
46
  validatorId: r.validatorId,
@@ -35,7 +51,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
35
51
 
36
52
  async runAllValidators(): Promise<ValidatorCheckItem[]> {
37
53
  const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
38
- const mod = createValidatorSystemModule();
54
+ const mod = createValidatorSystemModule(await loadValidatorSystemConfig());
39
55
  const report = await mod.runFullValidationUseCase.execute({ targetPaths: [], unitName: '', currentPhase: '' });
40
56
  return report.results.map((r) => ({
41
57
  validatorId: r.validatorId,
@@ -48,7 +64,7 @@ export class ValidatorSystemExecutionAdapter implements ValidatorExecutionPort {
48
64
  // ISSUE-005 P1-5: L4-001 の DriftDetectionService を直接呼び出し、
49
65
  // phasegate:detect-drift と validate --layer L4 の結果を一致させる
50
66
  const { createValidatorSystemModule } = await import('../../../validator-system/composition-root.js');
51
- const mod = createValidatorSystemModule();
67
+ const mod = createValidatorSystemModule(await loadValidatorSystemConfig());
52
68
  const reports = await mod.driftDetectionService.detect();
53
69
  return reports.map((r) => ({
54
70
  direction: r.direction,
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @unit harness-api
3
3
  * @layer presentation
4
+ * @work-item-id WI-092
4
5
  *
5
6
  * Pre-commit CLI entry.
6
7
  * Runs L2 validators against staged TypeScript files AND design-document
@@ -15,6 +16,9 @@
15
16
 
16
17
  import { execSync } from "node:child_process";
17
18
  import { readFile } from "node:fs/promises";
19
+ import { createConfigFoundationModule } from "../config-foundation/composition-root.js";
20
+ import { toValidatorSystemConfig } from "../config-foundation/application/mappers/validator-system-config-mapper.js";
21
+ import { ConfigNotFoundError } from "../config-foundation/infrastructure/repositories/file-system-config-repository.js";
18
22
  import { createTraceabilityModelModule } from "../traceability-model/composition-root.js";
19
23
  import type { ValidateMetadataCommandOutput } from "../traceability-model/presentation/cli/validate-metadata-command-handler.js";
20
24
  import type { AggregatedValidationReport } from "../validator-system/application/dto/aggregated-validation-report.js";
@@ -34,6 +38,30 @@ const WORK_ITEM_PATH_PATTERN = /(?:^|\/)WI-\d+(?:\/|$)/;
34
38
  const WORK_ITEM_TRAILER_PATTERN = /^Work-Item:\s*WI-\d+\s*$/m;
35
39
  const TEST_FILE_SUFFIXES = Object.freeze([".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"]);
36
40
 
41
+ async function loadValidatorSystemConfig(): Promise<object | undefined> {
42
+ const configMod = createConfigFoundationModule();
43
+ try {
44
+ const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
45
+ return toValidatorSystemConfig(resolvedConfig.config);
46
+ } catch (err) {
47
+ if (err instanceof ConfigNotFoundError) return undefined;
48
+ throw err;
49
+ }
50
+ }
51
+
52
+ async function loadTraceabilityModelOptions(): Promise<
53
+ { readonly pathRoots: { readonly designDocsRoot: string } } | undefined
54
+ > {
55
+ const configMod = createConfigFoundationModule();
56
+ try {
57
+ const resolvedConfig = await configMod.usecases.loadResolvedConfigUseCase.execute();
58
+ return { pathRoots: { designDocsRoot: resolvedConfig.config.paths.designDocs } };
59
+ } catch (err) {
60
+ if (err instanceof ConfigNotFoundError) return undefined;
61
+ throw err;
62
+ }
63
+ }
64
+
37
65
  function isTestFile(path: string): boolean {
38
66
  return TEST_FILE_SUFFIXES.some((suffix) => path.endsWith(suffix));
39
67
  }
@@ -303,8 +331,11 @@ export async function runPreCommit(
303
331
  export async function runPreCommitCli(): Promise<void> {
304
332
  try {
305
333
  const stagedFiles = getStagedFiles();
306
- const validatorMod = createValidatorSystemModule();
307
- const traceabilityMod = createTraceabilityModelModule(process.cwd());
334
+ const validatorMod = createValidatorSystemModule(await loadValidatorSystemConfig());
335
+ const traceabilityMod = createTraceabilityModelModule(
336
+ process.cwd(),
337
+ await loadTraceabilityModelOptions(),
338
+ );
308
339
 
309
340
  const result = await runPreCommit(
310
341
  stagedFiles,
@@ -335,8 +366,11 @@ export async function runCommitMsgCli(commitMessagePath: string | undefined): Pr
335
366
 
336
367
  const stagedFiles = getStagedFiles();
337
368
  const commitMessage = await readFile(commitMessagePath, "utf-8");
338
- const validatorMod = createValidatorSystemModule();
339
- const traceabilityMod = createTraceabilityModelModule(process.cwd());
369
+ const validatorMod = createValidatorSystemModule(await loadValidatorSystemConfig());
370
+ const traceabilityMod = createTraceabilityModelModule(
371
+ process.cwd(),
372
+ await loadTraceabilityModelOptions(),
373
+ );
340
374
 
341
375
  const result = await runPreCommit(
342
376
  stagedFiles,
@@ -14,6 +14,7 @@ import { createAdrFoundationModule } from "./adr-foundation/composition-root.js"
14
14
  import { createBiomeAstEngineModule } from "./biome-ast-engine/composition-root.js";
15
15
  import { buildCiGovernance } from "./ci-governance/composition-root.js";
16
16
  import { toPhaseConfigSection } from "./config-foundation/application/mappers/phase-config-section-mapper.js";
17
+ import { toValidatorSystemConfig } from "./config-foundation/application/mappers/validator-system-config-mapper.js";
17
18
  import { createConfigFoundationModule } from "./config-foundation/composition-root.js";
18
19
  import { ConfigValidationError } from "./config-foundation/domain/errors/config-validation-error.js";
19
20
  import type { HarnessConfigV2 } from "./config-foundation/domain/harness-config.js";
@@ -66,6 +67,12 @@ function getProjectRoot(): string {
66
67
  return process.cwd();
67
68
  }
68
69
 
70
+ function toTraceabilityModelOptions(resolvedConfig: HarnessConfigV2 | undefined) {
71
+ return resolvedConfig
72
+ ? { pathRoots: { designDocsRoot: resolvedConfig.paths.designDocs } }
73
+ : undefined;
74
+ }
75
+
69
76
  async function pathExists(path: string): Promise<boolean> {
70
77
  try {
71
78
  await access(path);
@@ -455,27 +462,6 @@ function toL1Config(resolvedConfig: HarnessConfigV2) {
455
462
  * HarnessConfigV2 (resolved) から biome-ast-engine が期待する architecture 情報を抽出する。
456
463
  * architecture が未設定の場合は undefined を返し、biome-ast-engine 側の default (clean) に委ねる。
457
464
  */
458
- /**
459
- * HarnessConfigV2 (resolved) から validator-system の `createValidatorSystemModule` が
460
- * 期待する shape (`{ project: { preset }, layers: { L2/L3/L4: { enabled } } }`) に変換する。
461
- *
462
- * WI-091 finding #1 follow-up: 直接 resolvedConfig を渡すと preset-style の
463
- * `validators: ["drift-detector"]` が validator-code (L4-001/002/003) を上書きし
464
- * 全 SKIP になる症状が出るため、`enabled` field のみ thread し validators の
465
- * 解決は composition root 側の defaultValidators[layer] フォールバックに委ねる。
466
- */
467
- function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefined): object | undefined {
468
- if (!resolvedConfig) return undefined;
469
- return {
470
- project: { preset: resolvedConfig.project.preset },
471
- layers: {
472
- L2: { enabled: resolvedConfig.layers.L2.enabled },
473
- L3: { enabled: resolvedConfig.layers.L3.enabled },
474
- L4: { enabled: resolvedConfig.layers.L4.enabled },
475
- },
476
- };
477
- }
478
-
479
465
  function toArchitectureInput(resolvedConfig: HarnessConfigV2) {
480
466
  if (!resolvedConfig.architecture) {
481
467
  return undefined;
@@ -854,7 +840,10 @@ async function main(): Promise<void> {
854
840
 
855
841
  case "migrate": {
856
842
  if (args[1] === "work-items") {
857
- const mod = createTraceabilityModelModule(rootDir);
843
+ const mod = createTraceabilityModelModule(
844
+ rootDir,
845
+ toTraceabilityModelOptions(resolvedConfig),
846
+ );
858
847
  const result = await mod.migrateWorkItemsCommandHandler.execute({
859
848
  dryRun: hasFlag(args, "--dry-run"),
860
849
  apply: hasFlag(args, "--apply"),
@@ -921,7 +910,10 @@ async function main(): Promise<void> {
921
910
 
922
911
  // ── traceability-model ──
923
912
  case "validate-metadata": {
924
- const mod = createTraceabilityModelModule(rootDir);
913
+ const mod = createTraceabilityModelModule(
914
+ rootDir,
915
+ toTraceabilityModelOptions(resolvedConfig),
916
+ );
925
917
  const filePaths = parsePositionalArgs(args.slice(1));
926
918
  const result = await mod.validateMetadataCommandHandler.execute({
927
919
  filePaths,
@@ -31,7 +31,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
31
31
  },
32
32
  {
33
33
  name: 'product-overview',
34
- path: 'docs/product/product_overview.md',
34
+ path: '{designDocsRoot}/../product_overview.md',
35
35
  required: true,
36
36
  },
37
37
  ]),
@@ -43,7 +43,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
43
43
  },
44
44
  {
45
45
  name: 'user-stories',
46
- path: 'docs/product/user_stories.md',
46
+ path: '{designDocsRoot}/../user_stories.md',
47
47
  required: true,
48
48
  },
49
49
  ]),
@@ -55,7 +55,7 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
55
55
  },
56
56
  {
57
57
  name: 'user-story-mapping',
58
- path: 'docs/product/user_story_mapping.md',
58
+ path: '{designDocsRoot}/../user_story_mapping.md',
59
59
  required: true,
60
60
  },
61
61
  ]),
@@ -67,12 +67,12 @@ export const FULL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
67
67
  },
68
68
  {
69
69
  name: 'unit-definition',
70
- path: 'docs/product/units/{unit}_unit.md',
70
+ path: '{designDocsRoot}/../units/{unit}_unit.md',
71
71
  required: true,
72
72
  },
73
73
  {
74
74
  name: 'integration-contract',
75
- path: 'docs/product/units/integration_contract.md',
75
+ path: '{designDocsRoot}/../units/integration_contract.md',
76
76
  required: true,
77
77
  },
78
78
  ]),
@@ -31,7 +31,7 @@ export const MINIMAL_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
31
31
  },
32
32
  {
33
33
  name: 'product-overview',
34
- path: 'docs/product/product_overview.md',
34
+ path: '{designDocsRoot}/../product_overview.md',
35
35
  required: true,
36
36
  },
37
37
  ]),
@@ -31,7 +31,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
31
31
  },
32
32
  {
33
33
  name: 'product-overview',
34
- path: 'docs/product/product_overview.md',
34
+ path: '{designDocsRoot}/../product_overview.md',
35
35
  required: true,
36
36
  },
37
37
  ]),
@@ -43,7 +43,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
43
43
  },
44
44
  {
45
45
  name: 'user-stories',
46
- path: 'docs/product/user_stories.md',
46
+ path: '{designDocsRoot}/../user_stories.md',
47
47
  required: true,
48
48
  },
49
49
  ]),
@@ -2,6 +2,7 @@
2
2
  * @layer domain
3
3
  * @unit phase-dependency-model
4
4
  */
5
+ import path from 'node:path';
5
6
 
6
7
  const ALLOWED_PLACEHOLDERS = new Set([
7
8
  'unit',
@@ -22,6 +23,8 @@ export const DEFAULT_PATH_ROOTS: PathRoots = Object.freeze({
22
23
  inceptionDocsRoot: 'docs/inception',
23
24
  });
24
25
 
26
+ const normalizeProjectPath = (value: string): string => path.posix.normalize(value);
27
+
25
28
  export interface ArtifactCreateArgs {
26
29
  readonly name: string;
27
30
  readonly path: string;
@@ -105,13 +108,15 @@ export class Artifact {
105
108
  throw new InvalidArtifactPathError(this.path);
106
109
  }
107
110
 
108
- return resolvedPath;
111
+ return normalizeProjectPath(resolvedPath);
109
112
  }
110
113
 
111
114
  expandRoots(pathRoots: PathRoots = DEFAULT_PATH_ROOTS): string {
112
- return this.path
113
- .replaceAll('{designDocsRoot}', pathRoots.designDocsRoot)
114
- .replaceAll('{inceptionDocsRoot}', pathRoots.inceptionDocsRoot);
115
+ return normalizeProjectPath(
116
+ this.path
117
+ .replaceAll('{designDocsRoot}', pathRoots.designDocsRoot)
118
+ .replaceAll('{inceptionDocsRoot}', pathRoots.inceptionDocsRoot),
119
+ );
115
120
  }
116
121
 
117
122
  equals(other: Artifact): boolean {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @layer composition
2
+ * @layer application
3
3
  * @unit traceability-model
4
4
  *
5
5
  * traceability-model ユニットの Composition Root。
@@ -25,12 +25,39 @@ import { MarkdownUnitDefinitionGateway } from "./infrastructure/gateways/markdow
25
25
  import { MigrateWorkItemsCommandHandler } from "./presentation/cli/migrate-work-items-command-handler.js";
26
26
  import { ValidateMetadataCommandHandler } from "./presentation/cli/validate-metadata-command-handler.js";
27
27
 
28
- export function createTraceabilityModelModule(rootDir: string) {
28
+ export interface TraceabilityModelPathRoots {
29
+ readonly designDocsRoot: string;
30
+ }
31
+
32
+ export interface TraceabilityModelModuleOptions {
33
+ readonly pathRoots?: TraceabilityModelPathRoots;
34
+ }
35
+
36
+ const deriveProductDocsRoot = (designDocsRoot: string): string => {
37
+ const normalized = designDocsRoot.replace(/\/+$/, "");
38
+ if (normalized.endsWith("/construction")) {
39
+ return normalized.slice(0, -"/construction".length);
40
+ }
41
+ return normalized;
42
+ };
43
+
44
+ export function createTraceabilityModelModule(
45
+ rootDir: string,
46
+ options: TraceabilityModelModuleOptions = {},
47
+ ) {
48
+ const designDocsRoot = options.pathRoots?.designDocsRoot ?? "docs/product/construction";
49
+ const productDocsRoot = deriveProductDocsRoot(designDocsRoot);
50
+ const storyCatalogPath = `${productDocsRoot}/user_stories.md`;
51
+
29
52
  // Infrastructure gateways
30
- const storyCatalog = new MarkdownStoryCatalogGateway({ rootDir });
31
- const unitDefinition = new MarkdownUnitDefinitionGateway({ rootDir });
53
+ const storyCatalog = new MarkdownStoryCatalogGateway({ rootDir, storyCatalogPath });
54
+ const unitDefinition = new MarkdownUnitDefinitionGateway({
55
+ rootDir,
56
+ productDocsRoot,
57
+ designDocsRoot,
58
+ });
32
59
  const metadataReader = new FileSystemMetadataReader({ rootDir });
33
- const designDocument = new MarkdownDesignDocumentGateway({ rootDir });
60
+ const designDocument = new MarkdownDesignDocumentGateway({ rootDir, designDocsRoot });
34
61
  const inceptionPlan = new FileSystemInceptionPlanGateway({ rootDir });
35
62
  const workItemMigrationSource = new FileSystemWorkItemMigrationSourceGateway({ rootDir });
36
63
  const workItemMigrationApply = new FileSystemWorkItemMigrationApplyGateway({ rootDir });
@@ -46,6 +73,7 @@ export function createTraceabilityModelModule(rootDir: string) {
46
73
  designDocumentPort: designDocument,
47
74
  storyCatalogPort: storyCatalog,
48
75
  inceptionPlanPort: inceptionPlan,
76
+ storyCatalogPath,
49
77
  });
50
78
  const storyIdAliasResolver = new StoryIdAliasResolver(storyCatalog);
51
79
 
@@ -17,7 +17,7 @@ import type { StoryIdLike } from '../value-objects/story-reference.js';
17
17
  import { TraceabilityChain } from '../value-objects/traceability-chain.js';
18
18
 
19
19
  const PROJECT_RELATIVE_PATH_PATTERN = /^(docs|scripts|inception)\//;
20
- const STORY_CATALOG_PATH = 'docs/product/user_stories.md';
20
+ const DEFAULT_STORY_CATALOG_PATH = 'docs/product/user_stories.md';
21
21
 
22
22
  const createPath = (value: string): ProjectRelativePathLike =>
23
23
  Object.freeze({
@@ -136,6 +136,7 @@ export class TraceabilityChainBuilder {
136
136
  private readonly designDocumentPort: DesignDocumentPort;
137
137
  private readonly storyCatalogPort: StoryCatalogPort;
138
138
  private readonly inceptionPlanPort: InceptionPlanPort;
139
+ private readonly storyCatalogPath: ProjectRelativePathLike;
139
140
 
140
141
  constructor(deps: {
141
142
  readonly metadataReaderPort: MetadataReaderPort;
@@ -143,12 +144,16 @@ export class TraceabilityChainBuilder {
143
144
  readonly designDocumentPort: DesignDocumentPort;
144
145
  readonly storyCatalogPort: StoryCatalogPort;
145
146
  readonly inceptionPlanPort: InceptionPlanPort;
147
+ readonly storyCatalogPath?: string;
146
148
  }) {
147
149
  this.metadataReaderPort = deps.metadataReaderPort;
148
150
  this.unitDefinitionPort = deps.unitDefinitionPort;
149
151
  this.designDocumentPort = deps.designDocumentPort;
150
152
  this.storyCatalogPort = deps.storyCatalogPort;
151
153
  this.inceptionPlanPort = deps.inceptionPlanPort;
154
+ this.storyCatalogPath = createPath(
155
+ deps.storyCatalogPath ?? DEFAULT_STORY_CATALOG_PATH,
156
+ );
152
157
  }
153
158
 
154
159
  async build(origin: string | ProjectRelativePathLike): Promise<TraceabilityChain> {
@@ -235,7 +240,7 @@ export class TraceabilityChainBuilder {
235
240
  designToStoryLinks.push(
236
241
  createLink({
237
242
  from: designDocument,
238
- to: createPath(STORY_CATALOG_PATH),
243
+ to: this.storyCatalogPath,
239
244
  linkType: 'design-to-story',
240
245
  resolved: false,
241
246
  }),
@@ -248,7 +253,7 @@ export class TraceabilityChainBuilder {
248
253
  designToStoryLinks.push(
249
254
  createLink({
250
255
  from: designDocument,
251
- to: createPath(STORY_CATALOG_PATH),
256
+ to: this.storyCatalogPath,
252
257
  linkType: 'design-to-story',
253
258
  resolved: storyResolved,
254
259
  }),
@@ -265,7 +270,7 @@ export class TraceabilityChainBuilder {
265
270
  );
266
271
  storyToPlanLinks.push(
267
272
  createLink({
268
- from: createPath(STORY_CATALOG_PATH),
273
+ from: this.storyCatalogPath,
269
274
  to:
270
275
  planRoot ??
271
276
  createPlaceholderPath(
@@ -2,10 +2,8 @@
2
2
  * @layer domain
3
3
  * @unit traceability-model
4
4
  *
5
- * docs/ または scripts/ 配下のPOSIX相対パス
5
+ * プロジェクト相対POSIXパス
6
6
  */
7
- const ALLOWED_PROJECT_ROOTS = new Set(['docs', 'scripts']);
8
-
9
7
  export class ProjectRelativePathError extends Error {
10
8
  readonly value: string;
11
9
 
@@ -42,7 +40,7 @@ const normalizeSegments = (rawValue: string): readonly string[] => {
42
40
  return segment;
43
41
  });
44
42
 
45
- if (segments.length === 0 || !ALLOWED_PROJECT_ROOTS.has(segments[0])) {
43
+ if (segments.length === 0) {
46
44
  throw new ProjectRelativePathError(rawValue);
47
45
  }
48
46
 
@@ -21,18 +21,22 @@ import { parseWorkItemFrontmatter } from "../parsers/work-item-frontmatter-parse
21
21
 
22
22
  export interface MarkdownDesignDocumentGatewayDeps {
23
23
  readonly rootDir: string;
24
+ readonly designDocsRoot?: string;
24
25
  }
25
26
 
26
27
  export class MarkdownDesignDocumentGateway implements DesignDocumentPort {
27
28
  private readonly rootDir: string;
29
+ private readonly designDocsRoot: string;
28
30
  private readonly contentCache = new Map<string, string>();
29
31
 
30
32
  constructor(deps: MarkdownDesignDocumentGatewayDeps) {
31
33
  this.rootDir = deps.rootDir;
34
+ this.designDocsRoot = deps.designDocsRoot ?? path.join("docs", "product", "construction");
32
35
  }
33
36
 
34
37
  async listByUnit(unitName: string): Promise<readonly ProjectRelativePathLike[]> {
35
- const constructionDir = path.join(this.rootDir, "docs", "product", "construction", unitName);
38
+ const constructionRoot = path.posix.join(this.designDocsRoot, unitName);
39
+ const constructionDir = path.join(this.rootDir, constructionRoot);
36
40
 
37
41
  if (!fs.existsSync(constructionDir)) {
38
42
  return [];
@@ -43,7 +47,7 @@ export class MarkdownDesignDocumentGateway implements DesignDocumentPort {
43
47
 
44
48
  for (const entry of entries) {
45
49
  if (entry.endsWith(".md")) {
46
- results.push(ProjectRelativePath.create(`docs/product/construction/${unitName}/${entry}`));
50
+ results.push(ProjectRelativePath.create(path.posix.join(constructionRoot, entry)));
47
51
  }
48
52
  }
49
53
 
@@ -13,16 +13,23 @@ import { parseStoryCatalog } from '../parsers/story-catalog-parser.js';
13
13
 
14
14
  export interface MarkdownStoryCatalogGatewayDeps {
15
15
  readonly rootDir: string;
16
+ readonly storyCatalogPath?: string;
16
17
  }
17
18
 
18
19
  export class MarkdownStoryCatalogGateway implements StoryCatalogPort {
19
20
  private readonly rootDir: string;
21
+ private readonly storyCatalogPath: string;
20
22
  private cachedStoryIds: readonly StoryIdLike[] | null = null;
21
23
  private cachedAliasMap: ReadonlyMap<string, StoryIdLike> | null = null;
22
24
  private cachedMtime: number | null = null;
23
25
 
24
26
  constructor(deps: MarkdownStoryCatalogGatewayDeps) {
25
27
  this.rootDir = deps.rootDir;
28
+ this.storyCatalogPath = deps.storyCatalogPath ?? path.join(
29
+ 'docs',
30
+ 'product',
31
+ 'user_stories.md',
32
+ );
26
33
  }
27
34
 
28
35
  async getAllStoryIds(): Promise<readonly StoryIdLike[]> {
@@ -48,12 +55,7 @@ export class MarkdownStoryCatalogGateway implements StoryCatalogPort {
48
55
  }
49
56
 
50
57
  private async ensureLoaded(): Promise<void> {
51
- const filePath = path.join(
52
- this.rootDir,
53
- 'docs',
54
- 'product',
55
- 'user_stories.md',
56
- );
58
+ const filePath = path.join(this.rootDir, this.storyCatalogPath);
57
59
 
58
60
  let currentMtime: number;
59
61
  try {
@@ -15,14 +15,20 @@ const UNIT_ID_PATTERN = /Unit\s+ID\*{0,2}\s*[::]\s*(\S+)/;
15
15
 
16
16
  export interface MarkdownUnitDefinitionGatewayDeps {
17
17
  readonly rootDir: string;
18
+ readonly productDocsRoot?: string;
19
+ readonly designDocsRoot?: string;
18
20
  }
19
21
 
20
22
  export class MarkdownUnitDefinitionGateway implements UnitDefinitionPort {
21
23
  private readonly rootDir: string;
24
+ private readonly productDocsRoot: string;
25
+ private readonly designDocsRoot: string;
22
26
  private cachedUnitNames: readonly string[] | null = null;
23
27
 
24
28
  constructor(deps: MarkdownUnitDefinitionGatewayDeps) {
25
29
  this.rootDir = deps.rootDir;
30
+ this.productDocsRoot = deps.productDocsRoot ?? path.join('docs', 'product');
31
+ this.designDocsRoot = deps.designDocsRoot ?? path.join('docs', 'product', 'construction');
26
32
  }
27
33
 
28
34
  async getAllUnitNames(): Promise<readonly string[]> {
@@ -47,7 +53,7 @@ export class MarkdownUnitDefinitionGateway implements UnitDefinitionPort {
47
53
  return null;
48
54
  }
49
55
 
50
- const constructionPath = `docs/product/construction/${unitName}`;
56
+ const constructionPath = path.posix.join(this.designDocsRoot, unitName);
51
57
  const absolutePath = path.join(this.rootDir, constructionPath);
52
58
 
53
59
  if (!fs.existsSync(absolutePath)) {
@@ -68,7 +74,7 @@ export class MarkdownUnitDefinitionGateway implements UnitDefinitionPort {
68
74
  return;
69
75
  }
70
76
 
71
- const unitsDir = path.join(this.rootDir, 'docs', 'product', 'units');
77
+ const unitsDir = path.join(this.rootDir, this.productDocsRoot, 'units');
72
78
  if (!fs.existsSync(unitsDir)) {
73
79
  this.cachedUnitNames = Object.freeze([]);
74
80
  return;
@@ -110,7 +110,7 @@ export class ValidateMetadataCommandHandler {
110
110
  results: Object.freeze([]),
111
111
  text:
112
112
  `Error: invalid file path: "${err.value}"\n` +
113
- ` Hint: paths must be project-relative and start with 'docs/' or 'scripts/'.`,
113
+ ` Hint: paths must be project-relative POSIX paths without '..' segments.`,
114
114
  });
115
115
  }
116
116
  return Object.freeze({