phasegate 0.129.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 +11 -1
- package/package.json +1 -1
- package/scripts/harness/config-foundation/index.ts +1 -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/integrations/pre-commit.ts +21 -2
- package/scripts/harness/main.ts +14 -2
- 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,16 @@ 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
|
+
|
|
10
20
|
## [0.129.0] - 2026-05-08
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -17,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
27
|
- **fallback**: `phasegate.config.json` 不在時のみ validator-system の default config にフォールバック。不正 config は従来通り runtime error として扱う。
|
|
18
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。
|
|
19
29
|
- **テスト**: `pnpm test` は 452 files / 3518 tests pass。対象テスト 3 files / 11 tests pass。変更ファイルの `validate-metadata` pass。
|
|
20
|
-
- **post-publish dogfood**:
|
|
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。
|
|
21
31
|
|
|
22
32
|
## [0.128.0] - 2026-05-08
|
|
23
33
|
|
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -49,6 +49,19 @@ async function loadValidatorSystemConfig(): Promise<object | undefined> {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
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
|
+
|
|
52
65
|
function isTestFile(path: string): boolean {
|
|
53
66
|
return TEST_FILE_SUFFIXES.some((suffix) => path.endsWith(suffix));
|
|
54
67
|
}
|
|
@@ -319,7 +332,10 @@ export async function runPreCommitCli(): Promise<void> {
|
|
|
319
332
|
try {
|
|
320
333
|
const stagedFiles = getStagedFiles();
|
|
321
334
|
const validatorMod = createValidatorSystemModule(await loadValidatorSystemConfig());
|
|
322
|
-
const traceabilityMod = createTraceabilityModelModule(
|
|
335
|
+
const traceabilityMod = createTraceabilityModelModule(
|
|
336
|
+
process.cwd(),
|
|
337
|
+
await loadTraceabilityModelOptions(),
|
|
338
|
+
);
|
|
323
339
|
|
|
324
340
|
const result = await runPreCommit(
|
|
325
341
|
stagedFiles,
|
|
@@ -351,7 +367,10 @@ export async function runCommitMsgCli(commitMessagePath: string | undefined): Pr
|
|
|
351
367
|
const stagedFiles = getStagedFiles();
|
|
352
368
|
const commitMessage = await readFile(commitMessagePath, "utf-8");
|
|
353
369
|
const validatorMod = createValidatorSystemModule(await loadValidatorSystemConfig());
|
|
354
|
-
const traceabilityMod = createTraceabilityModelModule(
|
|
370
|
+
const traceabilityMod = createTraceabilityModelModule(
|
|
371
|
+
process.cwd(),
|
|
372
|
+
await loadTraceabilityModelOptions(),
|
|
373
|
+
);
|
|
355
374
|
|
|
356
375
|
const result = await runPreCommit(
|
|
357
376
|
stagedFiles,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -67,6 +67,12 @@ function getProjectRoot(): string {
|
|
|
67
67
|
return process.cwd();
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function toTraceabilityModelOptions(resolvedConfig: HarnessConfigV2 | undefined) {
|
|
71
|
+
return resolvedConfig
|
|
72
|
+
? { pathRoots: { designDocsRoot: resolvedConfig.paths.designDocs } }
|
|
73
|
+
: undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
async function pathExists(path: string): Promise<boolean> {
|
|
71
77
|
try {
|
|
72
78
|
await access(path);
|
|
@@ -834,7 +840,10 @@ async function main(): Promise<void> {
|
|
|
834
840
|
|
|
835
841
|
case "migrate": {
|
|
836
842
|
if (args[1] === "work-items") {
|
|
837
|
-
const mod = createTraceabilityModelModule(
|
|
843
|
+
const mod = createTraceabilityModelModule(
|
|
844
|
+
rootDir,
|
|
845
|
+
toTraceabilityModelOptions(resolvedConfig),
|
|
846
|
+
);
|
|
838
847
|
const result = await mod.migrateWorkItemsCommandHandler.execute({
|
|
839
848
|
dryRun: hasFlag(args, "--dry-run"),
|
|
840
849
|
apply: hasFlag(args, "--apply"),
|
|
@@ -901,7 +910,10 @@ async function main(): Promise<void> {
|
|
|
901
910
|
|
|
902
911
|
// ── traceability-model ──
|
|
903
912
|
case "validate-metadata": {
|
|
904
|
-
const mod = createTraceabilityModelModule(
|
|
913
|
+
const mod = createTraceabilityModelModule(
|
|
914
|
+
rootDir,
|
|
915
|
+
toTraceabilityModelOptions(resolvedConfig),
|
|
916
|
+
);
|
|
905
917
|
const filePaths = parsePositionalArgs(args.slice(1));
|
|
906
918
|
const result = await mod.validateMetadataCommandHandler.execute({
|
|
907
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({
|