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.
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +19 -0
- package/scripts/harness/config-foundation/index.ts +2 -1
- package/scripts/harness/harness-api/infrastructure/adapters/nyquist-validation-impact-analysis-adapter.ts +7 -1
- package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts +6 -4
- package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +19 -3
- package/scripts/harness/integrations/pre-commit.ts +38 -4
- package/scripts/harness/main.ts +15 -23
- package/scripts/harness/phase-dependency-model/domain/definitions/full-phase-nodes.ts +5 -5
- package/scripts/harness/phase-dependency-model/domain/definitions/minimal-phase-nodes.ts +1 -1
- package/scripts/harness/phase-dependency-model/domain/definitions/standard-phase-nodes.ts +2 -2
- package/scripts/harness/phase-dependency-model/domain/values/artifact.ts +9 -4
- package/scripts/harness/traceability-model/composition-root.ts +33 -5
- package/scripts/harness/traceability-model/domain/services/traceability-chain-builder.ts +9 -4
- package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +2 -4
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-design-document-gateway.ts +6 -2
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-story-catalog-gateway.ts +8 -6
- package/scripts/harness/traceability-model/infrastructure/gateways/markdown-unit-definition-gateway.ts +8 -2
- 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
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
ADDED
|
@@ -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
|
|
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');
|
package/scripts/harness/harness-api/infrastructure/adapters/phase-dependency-model-query-adapter.ts
CHANGED
|
@@ -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,
|
package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
70
|
+
path: '{designDocsRoot}/../units/{unit}_unit.md',
|
|
71
71
|
required: true,
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
name: 'integration-contract',
|
|
75
|
-
path: '
|
|
75
|
+
path: '{designDocsRoot}/../units/integration_contract.md',
|
|
76
76
|
required: true,
|
|
77
77
|
},
|
|
78
78
|
]),
|
|
@@ -31,7 +31,7 @@ export const STANDARD_PHASE_NODES: readonly PhaseNode[] = Object.freeze([
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
name: 'product-overview',
|
|
34
|
-
path: '
|
|
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: '
|
|
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
|
|
113
|
-
.
|
|
114
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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(
|
|
50
|
+
results.push(ProjectRelativePath.create(path.posix.join(constructionRoot, entry)));
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
|
package/scripts/harness/traceability-model/infrastructure/gateways/markdown-story-catalog-gateway.ts
CHANGED
|
@@ -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 =
|
|
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,
|
|
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;
|
package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts
CHANGED
|
@@ -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
|
|
113
|
+
` Hint: paths must be project-relative POSIX paths without '..' segments.`,
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
return Object.freeze({
|