phasegate 0.137.0 → 0.138.1
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 +14 -0
- package/docs/folder_management_rules.md +4 -1
- package/package.json +1 -1
- package/scripts/harness/config-foundation/domain/harness-config.ts +4 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json +9 -0
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +9 -0
- package/scripts/harness/traceability-model/application/usecases/validate-design-story-annotations-usecase.ts +35 -0
- package/scripts/harness/traceability-model/composition-root.ts +3 -0
- package/scripts/harness/traceability-model/domain/ports/work-item-identity-port.ts +13 -0
- package/scripts/harness/traceability-model/domain/services/work-item-identity-validation-service.ts +46 -0
- package/scripts/harness/traceability-model/infrastructure/gateways/file-system-work-item-identity-gateway.ts +74 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **WI-106 — inception WI ID 重複防止** — `docs/inception/**/WI-XXX/description.md` の frontmatter `id` を global scan し、`_cross` と Unit 配下をまたぐ重複、および parent directory 名と `id` の不一致を `validate-metadata` 経路で検出するようにした。
|
|
13
|
+
- `docs/folder_management_rules.md` / `AGENTS.md` に、新規 WI 作成時の global unique ルールを明記。
|
|
14
|
+
- `migrate work-items` の採番が `_cross` と Unit 配下の既存 WI 番号を避けることを回帰テストで固定。
|
|
15
|
+
|
|
16
|
+
## [0.138.0] - 2026-05-09
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **WI-032 post-publish dogfood** — `phasegate init --with-ci` が生成する `phasegate.config.json` の `ci.enabled` を config schema v2/v3 で許可した。
|
|
21
|
+
- v0.137.0 published package の dogfood で、init 後の `ci:generate-template` / `ci:auto-refresh-agent-context` が `additionalProperties: /ci is not allowed` で失敗することを確認。
|
|
22
|
+
- `ci.enabled` は WI-031 以降の公開設定として生成済みのため、schema 側を追従させる。
|
|
23
|
+
|
|
10
24
|
## [0.137.0] - 2026-05-09
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -77,6 +77,8 @@ scripts/harness/{unit}/(domain|application|infrastructure|presentation)/*.ts
|
|
|
77
77
|
| `inception/_cross/{WI-XXX}/` | 複数 Unit に影響する cross-cutting WI | `_cross/WI-026/`, `_cross/WI-031/` |
|
|
78
78
|
| `inception/{unit}/{WI-XXX}/` | 単一 Unit が所有する WI | `validator-system/WI-074/` |
|
|
79
79
|
|
|
80
|
+
`WI-XXX` は `docs/inception/**` 全体で一意でなければなりません。`_cross/WI-031` と `{unit}/WI-031` のように配置先が違っても同じ ID は使えません。`description.md` の frontmatter `id` は parent directory 名と一致させます。
|
|
81
|
+
|
|
80
82
|
> **廃止された配置**(v0.104.0 で物理削除):
|
|
81
83
|
> - `docs/inception/issues/{ISSUE-XXX}/`
|
|
82
84
|
> - `docs/inception/{unit}/issues/{ISSUE-XXX}/`
|
|
@@ -266,7 +268,8 @@ TESTED @work-item-id 付きテストが存在し green
|
|
|
266
268
|
|
|
267
269
|
`migrate work-items --apply` および新規 WI 起票時の採番:
|
|
268
270
|
|
|
269
|
-
- 既存 WI
|
|
271
|
+
- 既存 WI 番号は `docs/inception/**/WI-XXX` 全体で予約(`_cross` と Unit 配下をまたいで重複しない)
|
|
272
|
+
- `description.md` の frontmatter `id` は parent directory 名 `WI-XXX` と一致させる
|
|
270
273
|
- 空き番号の **若い順**で sequential allocation
|
|
271
274
|
- legacy ID(`ISSUE-XXX` / `US-XXX` / `H{NN}-{NN}`)が存在する場合、frontmatter の `legacy_id` に保持
|
|
272
275
|
|
package/package.json
CHANGED
|
@@ -50,6 +50,7 @@ export interface HarnessConfigSourceDocument {
|
|
|
50
50
|
harnesses: Partial<HarnessConfigResolvedDocument['harnesses']>;
|
|
51
51
|
paths: HarnessConfigResolvedDocument['paths'];
|
|
52
52
|
reporting: HarnessConfigResolvedDocument['reporting'];
|
|
53
|
+
ci?: HarnessConfigResolvedDocument['ci'];
|
|
53
54
|
validate?: HarnessConfigResolvedDocument['validate'];
|
|
54
55
|
architecture?: ArchitectureConfigSource;
|
|
55
56
|
}
|
|
@@ -110,6 +111,9 @@ export interface HarnessConfigResolvedDocument {
|
|
|
110
111
|
format: string;
|
|
111
112
|
outputDir: string;
|
|
112
113
|
};
|
|
114
|
+
ci?: {
|
|
115
|
+
enabled: boolean;
|
|
116
|
+
};
|
|
113
117
|
validate: {
|
|
114
118
|
failOnWarning: boolean;
|
|
115
119
|
};
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v2.schema.json
CHANGED
|
@@ -439,6 +439,15 @@
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
},
|
|
442
|
+
"ci": {
|
|
443
|
+
"type": "object",
|
|
444
|
+
"additionalProperties": false,
|
|
445
|
+
"properties": {
|
|
446
|
+
"enabled": {
|
|
447
|
+
"type": "boolean"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
},
|
|
442
451
|
"validate": {
|
|
443
452
|
"type": "object",
|
|
444
453
|
"additionalProperties": false,
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json
CHANGED
|
@@ -439,6 +439,15 @@
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
},
|
|
442
|
+
"ci": {
|
|
443
|
+
"type": "object",
|
|
444
|
+
"additionalProperties": false,
|
|
445
|
+
"properties": {
|
|
446
|
+
"enabled": {
|
|
447
|
+
"type": "boolean"
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
},
|
|
442
451
|
"validate": {
|
|
443
452
|
"type": "object",
|
|
444
453
|
"additionalProperties": false,
|
|
@@ -12,6 +12,11 @@ import {
|
|
|
12
12
|
import type { ProjectRelativePath } from "../../domain/value-objects/project-relative-path.js";
|
|
13
13
|
import type { WorkItemFrontmatter } from "../../domain/value-objects/work-item-frontmatter.js";
|
|
14
14
|
import { WorkItemFrontmatterValidationError } from "../../domain/value-objects/work-item-frontmatter.js";
|
|
15
|
+
import type { WorkItemIdentityPort } from "../../domain/ports/work-item-identity-port.js";
|
|
16
|
+
import {
|
|
17
|
+
WorkItemIdentityValidationService,
|
|
18
|
+
type WorkItemIdentityViolation,
|
|
19
|
+
} from "../../domain/services/work-item-identity-validation-service.js";
|
|
15
20
|
import type { MetadataValidationOutput } from "../dto/metadata-validation-output.js";
|
|
16
21
|
|
|
17
22
|
type DesignStoryAnnotationsValidator = Pick<MetadataValidator, "validateDesignDocument">;
|
|
@@ -41,6 +46,20 @@ status: drafted
|
|
|
41
46
|
---`,
|
|
42
47
|
});
|
|
43
48
|
|
|
49
|
+
const toWorkItemIdentityError = (violation: WorkItemIdentityViolation): TraceabilityHarnessError =>
|
|
50
|
+
Object.freeze({
|
|
51
|
+
code: "L2-002",
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: violation.message,
|
|
54
|
+
suggestion: "docs/inception/**/WI-XXX/description.md の parent directory 名と frontmatter id を一致させ、WI id を inception 全体で一意にしてください",
|
|
55
|
+
fix_example: `---
|
|
56
|
+
id: ${violation.workItemId}
|
|
57
|
+
type: issue
|
|
58
|
+
severity: normal
|
|
59
|
+
status: drafted
|
|
60
|
+
---`,
|
|
61
|
+
});
|
|
62
|
+
|
|
44
63
|
const mergeValidationResult = (
|
|
45
64
|
additionalErrors: readonly TraceabilityHarnessError[],
|
|
46
65
|
result: MetadataValidationResult,
|
|
@@ -92,15 +111,22 @@ export class DesignDocumentReadApplicationError extends Error {
|
|
|
92
111
|
export interface ValidateDesignStoryAnnotationsUseCaseDeps {
|
|
93
112
|
readonly designDocumentPort: DesignDocumentPort;
|
|
94
113
|
readonly validator: DesignStoryAnnotationsValidator;
|
|
114
|
+
readonly workItemIdentityPort?: WorkItemIdentityPort;
|
|
115
|
+
readonly workItemIdentityValidationService?: WorkItemIdentityValidationService;
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
export class ValidateDesignStoryAnnotationsUseCase {
|
|
98
119
|
private readonly designDocumentPort: DesignDocumentPort;
|
|
99
120
|
private readonly validator: DesignStoryAnnotationsValidator;
|
|
121
|
+
private readonly workItemIdentityPort?: WorkItemIdentityPort;
|
|
122
|
+
private readonly workItemIdentityValidationService: WorkItemIdentityValidationService;
|
|
100
123
|
|
|
101
124
|
constructor(deps: ValidateDesignStoryAnnotationsUseCaseDeps) {
|
|
102
125
|
this.designDocumentPort = deps.designDocumentPort;
|
|
103
126
|
this.validator = deps.validator;
|
|
127
|
+
this.workItemIdentityPort = deps.workItemIdentityPort;
|
|
128
|
+
this.workItemIdentityValidationService =
|
|
129
|
+
deps.workItemIdentityValidationService ?? new WorkItemIdentityValidationService();
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
async execute(filePaths: readonly ProjectRelativePath[]): Promise<readonly Readonly<MetadataValidationOutput>[]> {
|
|
@@ -128,6 +154,11 @@ export class ValidateDesignStoryAnnotationsUseCase {
|
|
|
128
154
|
}
|
|
129
155
|
}
|
|
130
156
|
}
|
|
157
|
+
if (this.workItemIdentityPort && isWorkItemDescriptionPath(filePath)) {
|
|
158
|
+
const entries = await this.workItemIdentityPort.listWorkItemIdentities();
|
|
159
|
+
const violations = this.workItemIdentityValidationService.validate(entries);
|
|
160
|
+
workItemFrontmatterErrors.push(...violations.map(toWorkItemIdentityError));
|
|
161
|
+
}
|
|
131
162
|
|
|
132
163
|
const flags = await this.designDocumentPort.readFrontmatterFlags(filePath);
|
|
133
164
|
const annotations = await this.designDocumentPort.readStoryAnnotations(filePath);
|
|
@@ -146,3 +177,7 @@ export class ValidateDesignStoryAnnotationsUseCase {
|
|
|
146
177
|
return Object.freeze(results);
|
|
147
178
|
}
|
|
148
179
|
}
|
|
180
|
+
|
|
181
|
+
function isWorkItemDescriptionPath(filePath: ProjectRelativePath): boolean {
|
|
182
|
+
return /(?:^|\/)WI-\d+\/description\.md$/.test(filePath.toString());
|
|
183
|
+
}
|
|
@@ -19,6 +19,7 @@ import { FileSystemInceptionPlanGateway } from "./infrastructure/gateways/file-s
|
|
|
19
19
|
import { FileSystemMetadataReader } from "./infrastructure/gateways/file-system-metadata-reader.js";
|
|
20
20
|
import { FileSystemWorkItemMigrationApplyGateway } from "./infrastructure/gateways/file-system-work-item-migration-apply-gateway.js";
|
|
21
21
|
import { FileSystemWorkItemMigrationSourceGateway } from "./infrastructure/gateways/file-system-work-item-migration-source-gateway.js";
|
|
22
|
+
import { FileSystemWorkItemIdentityGateway } from "./infrastructure/gateways/file-system-work-item-identity-gateway.js";
|
|
22
23
|
import { MarkdownDesignDocumentGateway } from "./infrastructure/gateways/markdown-design-document-gateway.js";
|
|
23
24
|
import { MarkdownStoryCatalogGateway } from "./infrastructure/gateways/markdown-story-catalog-gateway.js";
|
|
24
25
|
import { MarkdownUnitDefinitionGateway } from "./infrastructure/gateways/markdown-unit-definition-gateway.js";
|
|
@@ -61,6 +62,7 @@ export function createTraceabilityModelModule(
|
|
|
61
62
|
const inceptionPlan = new FileSystemInceptionPlanGateway({ rootDir });
|
|
62
63
|
const workItemMigrationSource = new FileSystemWorkItemMigrationSourceGateway({ rootDir });
|
|
63
64
|
const workItemMigrationApply = new FileSystemWorkItemMigrationApplyGateway({ rootDir });
|
|
65
|
+
const workItemIdentity = new FileSystemWorkItemIdentityGateway({ rootDir });
|
|
64
66
|
|
|
65
67
|
// Domain services
|
|
66
68
|
const metadataValidator = new MetadataValidator({
|
|
@@ -85,6 +87,7 @@ export function createTraceabilityModelModule(
|
|
|
85
87
|
const validateDesignStoryAnnotationsUseCase = new ValidateDesignStoryAnnotationsUseCase({
|
|
86
88
|
designDocumentPort: designDocument,
|
|
87
89
|
validator: metadataValidator,
|
|
90
|
+
workItemIdentityPort: workItemIdentity,
|
|
88
91
|
});
|
|
89
92
|
const validateTestStoryMetadataUseCase = new ValidateTestStoryMetadataUseCase({
|
|
90
93
|
metadataReaderPort: metadataReader,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-106
|
|
4
|
+
|
|
5
|
+
export interface WorkItemIdentityEntry {
|
|
6
|
+
readonly filePath: string;
|
|
7
|
+
readonly directoryId: string;
|
|
8
|
+
readonly frontmatterId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WorkItemIdentityPort {
|
|
12
|
+
listWorkItemIdentities(): Promise<readonly WorkItemIdentityEntry[]>;
|
|
13
|
+
}
|
package/scripts/harness/traceability-model/domain/services/work-item-identity-validation-service.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-106
|
|
4
|
+
|
|
5
|
+
import type { WorkItemIdentityEntry } from "../ports/work-item-identity-port.js";
|
|
6
|
+
|
|
7
|
+
export interface WorkItemIdentityViolation {
|
|
8
|
+
readonly code: "duplicate-id" | "directory-id-mismatch";
|
|
9
|
+
readonly workItemId: string;
|
|
10
|
+
readonly filePaths: readonly string[];
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class WorkItemIdentityValidationService {
|
|
15
|
+
validate(entries: readonly WorkItemIdentityEntry[]): readonly WorkItemIdentityViolation[] {
|
|
16
|
+
const violations: WorkItemIdentityViolation[] = [];
|
|
17
|
+
const byId = new Map<string, WorkItemIdentityEntry[]>();
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.directoryId !== entry.frontmatterId) {
|
|
21
|
+
violations.push({
|
|
22
|
+
code: "directory-id-mismatch",
|
|
23
|
+
workItemId: entry.frontmatterId,
|
|
24
|
+
filePaths: Object.freeze([entry.filePath]),
|
|
25
|
+
message: `WI directory id ${entry.directoryId} does not match frontmatter id ${entry.frontmatterId}: ${entry.filePath}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const existing = byId.get(entry.frontmatterId) ?? [];
|
|
30
|
+
existing.push(entry);
|
|
31
|
+
byId.set(entry.frontmatterId, existing);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [workItemId, sameIdEntries] of byId.entries()) {
|
|
35
|
+
if (sameIdEntries.length <= 1) continue;
|
|
36
|
+
violations.push({
|
|
37
|
+
code: "duplicate-id",
|
|
38
|
+
workItemId,
|
|
39
|
+
filePaths: Object.freeze(sameIdEntries.map((entry) => entry.filePath).sort()),
|
|
40
|
+
message: `WI id ${workItemId} is duplicated: ${sameIdEntries.map((entry) => entry.filePath).sort().join(", ")}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Object.freeze(violations);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @unit traceability-model
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-106
|
|
4
|
+
|
|
5
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import type {
|
|
8
|
+
WorkItemIdentityEntry,
|
|
9
|
+
WorkItemIdentityPort,
|
|
10
|
+
} from "../../domain/ports/work-item-identity-port.js";
|
|
11
|
+
import { parseWorkItemFrontmatter } from "../parsers/work-item-frontmatter-parser.js";
|
|
12
|
+
|
|
13
|
+
const WI_DIR_PATTERN = /^WI-\d+$/;
|
|
14
|
+
const SKIPPED_DIRS = new Set(["archive", "_shared", "_operation", "share"]);
|
|
15
|
+
|
|
16
|
+
export interface FileSystemWorkItemIdentityGatewayDeps {
|
|
17
|
+
readonly rootDir: string;
|
|
18
|
+
readonly inceptionRoot?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FileSystemWorkItemIdentityGateway implements WorkItemIdentityPort {
|
|
22
|
+
private readonly rootDir: string;
|
|
23
|
+
private readonly inceptionRoot: string;
|
|
24
|
+
|
|
25
|
+
constructor(deps: FileSystemWorkItemIdentityGatewayDeps) {
|
|
26
|
+
this.rootDir = deps.rootDir;
|
|
27
|
+
this.inceptionRoot = deps.inceptionRoot ?? "docs/inception";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async listWorkItemIdentities(): Promise<readonly WorkItemIdentityEntry[]> {
|
|
31
|
+
const results: WorkItemIdentityEntry[] = [];
|
|
32
|
+
await this.collect(this.inceptionRoot, results);
|
|
33
|
+
return Object.freeze(results.sort((a, b) => a.filePath.localeCompare(b.filePath)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async collect(relativeDir: string, results: WorkItemIdentityEntry[]): Promise<void> {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await readdir(path.join(this.rootDir, relativeDir), { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry.isDirectory()) continue;
|
|
46
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
47
|
+
|
|
48
|
+
const childDir = path.posix.join(relativeDir, entry.name);
|
|
49
|
+
if (WI_DIR_PATTERN.test(entry.name)) {
|
|
50
|
+
const descriptionPath = path.posix.join(childDir, "description.md");
|
|
51
|
+
const frontmatter = await this.readFrontmatterId(descriptionPath);
|
|
52
|
+
if (frontmatter !== null) {
|
|
53
|
+
results.push({
|
|
54
|
+
filePath: descriptionPath,
|
|
55
|
+
directoryId: entry.name,
|
|
56
|
+
frontmatterId: frontmatter,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await this.collect(childDir, results);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private async readFrontmatterId(descriptionPath: string): Promise<string | null> {
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(path.join(this.rootDir, descriptionPath), "utf8");
|
|
69
|
+
return parseWorkItemFrontmatter(content)?.id ?? null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|