phasegate 0.160.20 → 0.160.21
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 +4 -0
- package/docs/templates/personal/hooks/pre-commit +16 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +19 -2
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +1 -3
- package/scripts/harness/main.ts +40 -18
- package/scripts/harness/validator-system/application/dto/validation-result-contract.ts +1 -0
- package/scripts/harness/validator-system/application/mappers/validation-result-contract-mapper.ts +1 -0
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +32 -3
- package/scripts/harness/validator-system/composition-root.ts +15 -1
- package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +78 -1
- package/scripts/harness/validator-system/domain/value-objects/validation-result.ts +7 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-work-item-reflection-adapter.ts +102 -0
- package/scripts/harness/validator-system/presentation/formatters/agent-validation-result-formatter.ts +3 -0
- package/scripts/harness/validator-system/presentation/formatters/human-validation-result-formatter.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,10 @@ 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-217 — personal inception/product consistency** — makes L4-002 validate configured personal documentation roots, scopes L4-004 freshness to `paths.designDocs`, adds personal hook L4 backstop behavior for `.phasegate-local` docs, and lets `scaffold-wi` use custom IDs and personal inception roots.
|
|
13
|
+
|
|
10
14
|
## [0.160.16] - 2026-05-22
|
|
11
15
|
|
|
12
16
|
### Fixed
|
|
@@ -18,4 +18,20 @@ if [ "${HARNESS_QUICK_MODE:-0}" = "1" ]; then
|
|
|
18
18
|
else
|
|
19
19
|
$PHASEGATE_CMD validate --layer L2 --format human
|
|
20
20
|
fi
|
|
21
|
+
L2_EXIT=$?
|
|
22
|
+
if [ $L2_EXIT -ne 0 ]; then
|
|
23
|
+
echo "PhaseGate L2 validation failed."
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)"
|
|
28
|
+
if printf '%s\n' "$STAGED_FILES" | grep -E '^\.phasegate-local/(inception|product)/' >/dev/null 2>&1; then
|
|
29
|
+
$PHASEGATE_CMD validate --layer L4 --format human --fail-on-warning
|
|
30
|
+
L4_EXIT=$?
|
|
31
|
+
if [ $L4_EXIT -ne 0 ]; then
|
|
32
|
+
echo "PhaseGate local inception/product consistency failed."
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
fi
|
|
21
36
|
|
|
37
|
+
exit 0
|
package/package.json
CHANGED
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit config-foundation
|
|
4
4
|
* @work-item-id WI-133 / WI-156
|
|
5
|
+
* @work-item-id WI-217
|
|
5
6
|
*/
|
|
6
7
|
import type { HarnessConfigV2 } from '../../domain/harness-config.js';
|
|
7
8
|
|
|
@@ -27,19 +28,26 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
|
|
|
27
28
|
'pointer-validator': 'L4-005',
|
|
28
29
|
'skill-catalog-drift': 'L4-006',
|
|
29
30
|
}, /^L4-\d{3}$/);
|
|
31
|
+
const effectiveL4Validators = usesCustomDocumentRoots(resolvedConfig)
|
|
32
|
+
? includeValidator(l4Validators, 'L4-002')
|
|
33
|
+
: l4Validators;
|
|
30
34
|
|
|
31
35
|
return {
|
|
32
36
|
project: { preset: resolvedConfig.project.preset },
|
|
37
|
+
paths: {
|
|
38
|
+
designDocs: resolvedConfig.paths.designDocs,
|
|
39
|
+
inceptionDocs: resolvedConfig.paths.inceptionDocs,
|
|
40
|
+
},
|
|
33
41
|
layers: {
|
|
34
42
|
L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
|
|
35
43
|
L3: {
|
|
36
44
|
enabled: resolvedConfig.layers.L3.enabled,
|
|
37
|
-
|
|
45
|
+
validators: l3Validators.length > 0 ? l3Validators : ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
|
|
38
46
|
coverageThreshold: resolvedConfig.layers.L3.coverageThreshold,
|
|
39
47
|
},
|
|
40
48
|
L4: {
|
|
41
49
|
enabled: resolvedConfig.layers.L4.enabled,
|
|
42
|
-
|
|
50
|
+
validators: effectiveL4Validators.length > 0 ? effectiveL4Validators : ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'],
|
|
43
51
|
},
|
|
44
52
|
},
|
|
45
53
|
harnesses: {
|
|
@@ -53,6 +61,15 @@ export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefi
|
|
|
53
61
|
};
|
|
54
62
|
}
|
|
55
63
|
|
|
64
|
+
function usesCustomDocumentRoots(resolvedConfig: HarnessConfigV2): boolean {
|
|
65
|
+
return resolvedConfig.paths.designDocs !== 'docs/product/construction'
|
|
66
|
+
|| resolvedConfig.paths.inceptionDocs !== 'docs/inception';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function includeValidator(validators: readonly string[], validatorId: string): readonly string[] {
|
|
70
|
+
return validators.includes(validatorId) ? validators : [...validators, validatorId];
|
|
71
|
+
}
|
|
72
|
+
|
|
56
73
|
function normalizeValidators(
|
|
57
74
|
validators: readonly string[],
|
|
58
75
|
aliases: Readonly<Record<string, string>>,
|
package/scripts/harness/main.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* @work-item-id WI-205
|
|
19
19
|
* @work-item-id WI-206
|
|
20
20
|
* @work-item-id WI-213
|
|
21
|
+
* @work-item-id WI-217
|
|
21
22
|
*
|
|
22
23
|
* Phasegate CLI エントリポイント。
|
|
23
24
|
* 各Unitの Composition Root からハンドラーを取得し、コマンドに応じてディスパッチする。
|
|
@@ -169,7 +170,7 @@ Setup:
|
|
|
169
170
|
--with-husky, --with-ci, --yes)
|
|
170
171
|
update-skills Alias for reconcile (kept for compatibility)
|
|
171
172
|
doctor Diagnose silent installation failures (--json, --strict, --personal, --agent <claude|codex|both>, --report-out <path>)
|
|
172
|
-
scaffold-wi <unit|_cross> <story|issue|chore>
|
|
173
|
+
scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore>
|
|
173
174
|
Create docs/inception/{unit}/WI-XXX/description.md
|
|
174
175
|
emit-agent-rules Print AGENTS.md / CLAUDE.md WI workflow rules block
|
|
175
176
|
install Install phasegate managed files (--dry-run|--apply, --force, --personal, --agent <claude|codex|both>)
|
|
@@ -280,14 +281,14 @@ function hasFlag(args: readonly string[], flag: string): boolean {
|
|
|
280
281
|
}
|
|
281
282
|
|
|
282
283
|
type WorkflowMode = "standard" | "strict";
|
|
283
|
-
type ScaffoldWorkItemType = "story" | "issue" | "chore";
|
|
284
|
+
type ScaffoldWorkItemType = "story" | "issue" | "fix" | "refactor" | "chore";
|
|
284
285
|
|
|
285
286
|
function parseWorkflowMode(value: string | undefined): WorkflowMode {
|
|
286
287
|
return value === "strict" ? "strict" : "standard";
|
|
287
288
|
}
|
|
288
289
|
|
|
289
290
|
function parseScaffoldWorkItemType(value: string | undefined): ScaffoldWorkItemType | null {
|
|
290
|
-
if (value === "story" || value === "issue" || value === "chore") return value;
|
|
291
|
+
if (value === "story" || value === "issue" || value === "fix" || value === "refactor" || value === "chore") return value;
|
|
291
292
|
return null;
|
|
292
293
|
}
|
|
293
294
|
|
|
@@ -319,8 +320,8 @@ async function listFilesRecursive(root: string): Promise<string[]> {
|
|
|
319
320
|
}
|
|
320
321
|
}
|
|
321
322
|
|
|
322
|
-
async function nextWorkItemId(rootDir: string): Promise<string> {
|
|
323
|
-
const files = await listFilesRecursive(join(rootDir,
|
|
323
|
+
async function nextWorkItemId(rootDir: string, inceptionRoot = "docs/inception"): Promise<string> {
|
|
324
|
+
const files = await listFilesRecursive(join(rootDir, inceptionRoot));
|
|
324
325
|
let max = 0;
|
|
325
326
|
for (const file of files) {
|
|
326
327
|
const match = file.match(/\/WI-(\d{3})\/description\.md$/);
|
|
@@ -336,19 +337,25 @@ async function countLegacyPlansWithoutWorkItems(rootDir: string): Promise<number
|
|
|
336
337
|
return files.filter((file) => file.includes("/codding_plan/") || file.endsWith("_plan.md")).length;
|
|
337
338
|
}
|
|
338
339
|
|
|
339
|
-
async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null): Promise<void> {
|
|
340
|
-
await fsMkdir(join(rootDir,
|
|
341
|
-
await fsMkdir(join(rootDir,
|
|
340
|
+
async function scaffoldInceptionRoots(rootDir: string, unit: string | null = null, inceptionRoot = "docs/inception"): Promise<void> {
|
|
341
|
+
await fsMkdir(join(rootDir, inceptionRoot, "_shared"), { recursive: true });
|
|
342
|
+
await fsMkdir(join(rootDir, inceptionRoot, "_cross"), { recursive: true });
|
|
342
343
|
if (unit && unit !== "_cross" && unit !== "_shared") {
|
|
343
|
-
await fsMkdir(join(rootDir,
|
|
344
|
-
await fsWriteFile(join(rootDir,
|
|
344
|
+
await fsMkdir(join(rootDir, inceptionRoot, unit), { recursive: true });
|
|
345
|
+
await fsWriteFile(join(rootDir, inceptionRoot, unit, ".gitkeep"), "", "utf8").catch(() => undefined);
|
|
345
346
|
}
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
async function scaffoldWorkItem(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
async function scaffoldWorkItem(
|
|
350
|
+
rootDir: string,
|
|
351
|
+
unit: string,
|
|
352
|
+
type: ScaffoldWorkItemType,
|
|
353
|
+
options: { readonly id?: string; readonly inceptionRoot?: string } = {},
|
|
354
|
+
): Promise<string> {
|
|
355
|
+
const inceptionRoot = options.inceptionRoot ?? "docs/inception";
|
|
356
|
+
const id = options.id ?? await nextWorkItemId(rootDir, inceptionRoot);
|
|
357
|
+
await scaffoldInceptionRoots(rootDir, unit, inceptionRoot);
|
|
358
|
+
const targetBase = unit === "_cross" ? join(rootDir, inceptionRoot, "_cross") : join(rootDir, inceptionRoot, unit);
|
|
352
359
|
const targetDir = join(targetBase, id);
|
|
353
360
|
await fsMkdir(targetDir, { recursive: true });
|
|
354
361
|
const descriptionPath = join(targetDir, "description.md");
|
|
@@ -754,15 +761,18 @@ Options:
|
|
|
754
761
|
Examples:
|
|
755
762
|
phasegate check-change-category --paths src/foo.ts,src/bar.ts
|
|
756
763
|
phasegate check-change-category --paths src/foo.ts --format json`,
|
|
757
|
-
"scaffold-wi": `Usage: phasegate scaffold-wi <unit|_cross> <story|issue|chore>
|
|
764
|
+
"scaffold-wi": `Usage: phasegate scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore> [options]
|
|
758
765
|
|
|
759
766
|
Create docs/inception/{unit}/WI-XXX/description.md.
|
|
760
767
|
|
|
761
768
|
Arguments:
|
|
762
769
|
<unit|_cross> Unit id or _cross for cross-cutting work items.
|
|
763
|
-
<story|issue|chore>
|
|
770
|
+
<story|issue|fix|refactor|chore>
|
|
771
|
+
Work item type.
|
|
764
772
|
|
|
765
773
|
Options:
|
|
774
|
+
--id <work-item-id> Use the supplied id instead of allocating WI-XXX.
|
|
775
|
+
--root <path> Inception root. Defaults to docs/inception, or personal paths.inceptionDocs.
|
|
766
776
|
--help, -h Show this help`,
|
|
767
777
|
"scaffold-design": `Usage: phasegate scaffold-design --unit <id> --phase <phase> [options]
|
|
768
778
|
|
|
@@ -2035,13 +2045,25 @@ async function main(): Promise<void> {
|
|
|
2035
2045
|
}
|
|
2036
2046
|
|
|
2037
2047
|
case "scaffold-wi": {
|
|
2048
|
+
const KNOWN_SCAFFOLD_WI_FLAGS = ["--id", "--root"];
|
|
2049
|
+
const flagError = validateKnownFlags(args, KNOWN_SCAFFOLD_WI_FLAGS);
|
|
2050
|
+
if (flagError) {
|
|
2051
|
+
console.error(flagError);
|
|
2052
|
+
process.exit(2);
|
|
2053
|
+
}
|
|
2038
2054
|
const unit = args[1];
|
|
2039
2055
|
const type = parseScaffoldWorkItemType(args[2]);
|
|
2040
2056
|
if (!unit || !type) {
|
|
2041
|
-
console.error("Usage: phasegate scaffold-wi <unit|_cross> <story|issue|chore>");
|
|
2057
|
+
console.error("Usage: phasegate scaffold-wi <unit|_cross> <story|issue|fix|refactor|chore> [--id <work-item-id>] [--root <path>]");
|
|
2042
2058
|
process.exit(2);
|
|
2043
2059
|
}
|
|
2044
|
-
const
|
|
2060
|
+
const configuredPersonalRoot = resolvedConfig?.paths.inceptionDocs?.startsWith(".phasegate-local/")
|
|
2061
|
+
? resolvedConfig.paths.inceptionDocs
|
|
2062
|
+
: undefined;
|
|
2063
|
+
const descriptionPath = await scaffoldWorkItem(rootDir, unit, type, {
|
|
2064
|
+
id: parseFlag(args, "--id"),
|
|
2065
|
+
inceptionRoot: parseFlag(args, "--root") ?? configuredPersonalRoot,
|
|
2066
|
+
});
|
|
2045
2067
|
console.log(`Created ${descriptionPath}`);
|
|
2046
2068
|
process.exit(0);
|
|
2047
2069
|
break;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit validator-system
|
|
4
4
|
* @work-item-id WI-107 / WI-156
|
|
5
|
+
* @work-item-id WI-217
|
|
5
6
|
*
|
|
6
7
|
* RunL4ValidatorsUseCase — H08-03: L4バリデータ実行
|
|
7
8
|
*/
|
|
@@ -70,6 +71,10 @@ export interface RunL4ValidatorsUseCaseDeps {
|
|
|
70
71
|
deadCodeDetectionService?: DeadCodeDetectionService;
|
|
71
72
|
architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
72
73
|
skillCatalogDriftPort?: SkillCatalogDriftPort;
|
|
74
|
+
pathRoots?: {
|
|
75
|
+
readonly inceptionRoot: string;
|
|
76
|
+
readonly designRoot: string;
|
|
77
|
+
};
|
|
73
78
|
checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
74
79
|
validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
75
80
|
}
|
|
@@ -85,6 +90,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
85
90
|
private readonly architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
86
91
|
private readonly skillCatalogDriftPort?: SkillCatalogDriftPort;
|
|
87
92
|
private readonly skillCatalogDriftService = new SkillCatalogDriftService();
|
|
93
|
+
private readonly pathRoots: { readonly inceptionRoot: string; readonly designRoot: string };
|
|
88
94
|
private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
89
95
|
private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
90
96
|
|
|
@@ -98,6 +104,10 @@ export class RunL4ValidatorsUseCase {
|
|
|
98
104
|
this.deadCodeDetectionService = deps.deadCodeDetectionService;
|
|
99
105
|
this.architectureSemanticAnalysisService = deps.architectureSemanticAnalysisService;
|
|
100
106
|
this.skillCatalogDriftPort = deps.skillCatalogDriftPort;
|
|
107
|
+
this.pathRoots = deps.pathRoots ?? {
|
|
108
|
+
inceptionRoot: 'docs/inception',
|
|
109
|
+
designRoot: 'docs/product/construction',
|
|
110
|
+
};
|
|
101
111
|
this.checkDocFreshnessUseCase = deps.checkDocFreshnessUseCase;
|
|
102
112
|
this.validateDocPointersUseCase = deps.validateDocPointersUseCase;
|
|
103
113
|
}
|
|
@@ -158,13 +168,25 @@ export class RunL4ValidatorsUseCase {
|
|
|
158
168
|
const l4002Result = overrideMap.get('L4-002');
|
|
159
169
|
if (l4002Result && !l4002Result.skipped) {
|
|
160
170
|
const report = await this.consistencyCheckService.check(input.targetUnits ? [...input.targetUnits] : undefined);
|
|
171
|
+
const reflectionResult = this.usesConfiguredDocumentRoots()
|
|
172
|
+
? await this.consistencyCheckService.checkWorkItemReflection(this.pathRoots)
|
|
173
|
+
: undefined;
|
|
161
174
|
const architectureSemanticErrors = this.architectureSemanticAnalysisService
|
|
162
175
|
? await this.architectureSemanticAnalysisService.analyze()
|
|
163
176
|
: [];
|
|
164
|
-
if (report.hasMismatches() || architectureSemanticErrors.length > 0) {
|
|
177
|
+
if (report.hasMismatches() || (reflectionResult?.report.hasMismatches() ?? false) || architectureSemanticErrors.length > 0) {
|
|
165
178
|
overrideMap.set(
|
|
166
179
|
'L4-002',
|
|
167
|
-
ValidationResult.fail(
|
|
180
|
+
ValidationResult.fail(
|
|
181
|
+
ValidatorId.create('L4-002'),
|
|
182
|
+
[...report.toHarnessErrors(), ...(reflectionResult?.report.toHarnessErrors() ?? []), ...architectureSemanticErrors],
|
|
183
|
+
0,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
} else if (reflectionResult?.skipReason && !report.hasMismatches() && architectureSemanticErrors.length === 0) {
|
|
187
|
+
overrideMap.set(
|
|
188
|
+
'L4-002',
|
|
189
|
+
ValidationResult.skipWithReason(ValidatorId.create('L4-002'), reflectionResult.skipReason),
|
|
168
190
|
);
|
|
169
191
|
}
|
|
170
192
|
}
|
|
@@ -187,7 +209,10 @@ export class RunL4ValidatorsUseCase {
|
|
|
187
209
|
if (this.checkDocFreshnessUseCase) {
|
|
188
210
|
const l4004Result = overrideMap.get('L4-004');
|
|
189
211
|
if (l4004Result && !l4004Result.skipped) {
|
|
190
|
-
const freshnessOutput = await this.checkDocFreshnessUseCase.execute({
|
|
212
|
+
const freshnessOutput = await this.checkDocFreshnessUseCase.execute({
|
|
213
|
+
format: 'json',
|
|
214
|
+
targetPattern: `${this.pathRoots.designRoot.replace(/\/+$/g, '')}/**/*.md`,
|
|
215
|
+
});
|
|
191
216
|
const errors = this.toDocFreshnessHarnessErrors(freshnessOutput);
|
|
192
217
|
overrideMap.set(
|
|
193
218
|
'L4-004',
|
|
@@ -251,6 +276,10 @@ export class RunL4ValidatorsUseCase {
|
|
|
251
276
|
return [...executionErrors, ...freshnessFindings];
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
private usesConfiguredDocumentRoots(): boolean {
|
|
280
|
+
return this.pathRoots.inceptionRoot !== 'docs/inception' || this.pathRoots.designRoot !== 'docs/product/construction';
|
|
281
|
+
}
|
|
282
|
+
|
|
254
283
|
private toPointerValidationHarnessErrors(output: ValidateDocPointersOutputContract): readonly ValidationResult['errors'][number][] {
|
|
255
284
|
const executionErrors = output.errors.map((error) => this.toHarnessErrorLike(
|
|
256
285
|
'L4-005',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* DI 組み立て — validator-system の全依存関係を構築する
|
|
6
6
|
* @work-item-id WI-110 / WI-111 / WI-132 / WI-133 / WI-136 / WI-137 / WI-138 / WI-156
|
|
7
|
+
* @work-item-id WI-217
|
|
7
8
|
*/
|
|
8
9
|
import { ValidatorId } from './domain/value-objects/validator-id.js';
|
|
9
10
|
import { ValidatorDefinition } from './domain/value-objects/validator-definition.js';
|
|
@@ -37,6 +38,7 @@ import { AdrFoundationReferenceAdapter } from './infrastructure/adapters/adr-fou
|
|
|
37
38
|
import { ImportGraphSourceAnalysisAdapter } from './infrastructure/adapters/import-graph-source-analysis-adapter.js';
|
|
38
39
|
import { FileSystemArchitectureSemanticSourceAdapter } from './infrastructure/adapters/file-system-architecture-semantic-source-adapter.js';
|
|
39
40
|
import { FileSystemSkillCatalogDriftAdapter } from './infrastructure/adapters/file-system-skill-catalog-drift-adapter.js';
|
|
41
|
+
import { FileSystemWorkItemReflectionAdapter } from './infrastructure/adapters/file-system-work-item-reflection-adapter.js';
|
|
40
42
|
import { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
|
|
41
43
|
import { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
|
|
42
44
|
import { DeadCodeDetectionService } from './domain/services/l4/dead-code-detection-service.js';
|
|
@@ -55,6 +57,10 @@ const DEFAULT_CONFIG = {
|
|
|
55
57
|
L3: { enabled: true, validators: ['L3-001', 'L3-002', 'L3-003', 'L3-004'], coverageThreshold: 90, bundleSizeLimit: 512000 },
|
|
56
58
|
L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005', 'L4-006'] },
|
|
57
59
|
},
|
|
60
|
+
paths: {
|
|
61
|
+
designDocs: 'docs/product/construction',
|
|
62
|
+
inceptionDocs: 'docs/inception',
|
|
63
|
+
},
|
|
58
64
|
validate: { failOnWarning: false },
|
|
59
65
|
architecture: {
|
|
60
66
|
capabilityPolicies: {
|
|
@@ -149,8 +155,10 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
149
155
|
const workItemStatusPolicyPort = new TraceabilityWorkItemStatusPolicyAdapter(process.cwd());
|
|
150
156
|
const contractTraceabilityPolicyPort = new FileSystemContractTraceabilityPolicyAdapter();
|
|
151
157
|
|
|
152
|
-
const docsRoot = join(process.cwd(), 'docs/product/construction');
|
|
153
158
|
const cwd = process.cwd();
|
|
159
|
+
const designDocsRoot = configData.paths?.designDocs ?? 'docs/product/construction';
|
|
160
|
+
const inceptionDocsRoot = configData.paths?.inceptionDocs ?? 'docs/inception';
|
|
161
|
+
const docsRoot = join(cwd, designDocsRoot);
|
|
154
162
|
const e2eTestFileRegistryPort = new E2eTestFileRegistryAdapter({ e2eTestRoot: join(cwd, 'scripts/harness/__tests__/e2e') });
|
|
155
163
|
const cliCommandRegistryPort = new CliCommandRegistryAdapter({
|
|
156
164
|
commands: [
|
|
@@ -191,6 +199,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
191
199
|
const sourceAnalysisPort = new ImportGraphSourceAnalysisAdapter();
|
|
192
200
|
const architectureSemanticSourcePort = new FileSystemArchitectureSemanticSourceAdapter();
|
|
193
201
|
const skillCatalogDriftPort = new FileSystemSkillCatalogDriftAdapter(cwd);
|
|
202
|
+
const workItemReflectionPort = new FileSystemWorkItemReflectionAdapter(cwd);
|
|
194
203
|
|
|
195
204
|
const driftDetectionService = new DriftDetectionService({
|
|
196
205
|
designDocumentPort: markdownDesignDocumentPort,
|
|
@@ -199,6 +208,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
199
208
|
const consistencyCheckService = new ConsistencyCheckService({
|
|
200
209
|
designDocumentPort: markdownDesignDocumentPort,
|
|
201
210
|
adrReferencePort,
|
|
211
|
+
workItemReflectionPort,
|
|
202
212
|
});
|
|
203
213
|
const deadCodeDetectionService = new DeadCodeDetectionService({
|
|
204
214
|
sourceAnalysisPort,
|
|
@@ -219,6 +229,10 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
219
229
|
deadCodeDetectionService,
|
|
220
230
|
architectureSemanticAnalysisService,
|
|
221
231
|
skillCatalogDriftPort,
|
|
232
|
+
pathRoots: {
|
|
233
|
+
inceptionRoot: inceptionDocsRoot,
|
|
234
|
+
designRoot: designDocsRoot,
|
|
235
|
+
},
|
|
222
236
|
checkDocFreshnessUseCase: phase2Extensions.checkDocFreshnessUseCase,
|
|
223
237
|
validateDocPointersUseCase: phase2Extensions.validateDocPointersUseCase,
|
|
224
238
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 設計文書間のレイヤー整合性検証(L4-002)
|
|
8
8
|
*/
|
|
9
9
|
import { ConsistencyReport } from '../../value-objects/consistency-report.js';
|
|
10
|
+
import type { MismatchPair } from '../../value-objects/consistency-report.js';
|
|
10
11
|
|
|
11
12
|
export interface ConsistencyDesignDocumentPort {
|
|
12
13
|
getLayerAnnotations(targetDocs?: readonly string[]): Promise<Record<string, string>>;
|
|
@@ -16,24 +17,47 @@ export interface ConsistencyAdrReferencePort {
|
|
|
16
17
|
exists(adrRef: string): Promise<boolean>;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export interface WorkItemReflectionSnapshot {
|
|
21
|
+
readonly workItems: readonly {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly type?: string;
|
|
25
|
+
}[];
|
|
26
|
+
readonly productRefs: readonly {
|
|
27
|
+
readonly id: string;
|
|
28
|
+
readonly path: string;
|
|
29
|
+
}[];
|
|
30
|
+
readonly skipReason?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkItemReflectionPort {
|
|
34
|
+
collect(input: {
|
|
35
|
+
readonly inceptionRoot: string;
|
|
36
|
+
readonly designRoot: string;
|
|
37
|
+
}): Promise<WorkItemReflectionSnapshot>;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
export interface ConsistencyCheckServiceDeps {
|
|
20
41
|
designDocumentPort: ConsistencyDesignDocumentPort;
|
|
21
42
|
adrReferencePort: ConsistencyAdrReferencePort;
|
|
43
|
+
workItemReflectionPort?: WorkItemReflectionPort;
|
|
22
44
|
}
|
|
23
45
|
|
|
24
46
|
export class ConsistencyCheckService {
|
|
25
47
|
private readonly designDocumentPort: ConsistencyDesignDocumentPort;
|
|
26
48
|
private readonly adrReferencePort: ConsistencyAdrReferencePort;
|
|
49
|
+
private readonly workItemReflectionPort?: WorkItemReflectionPort;
|
|
27
50
|
|
|
28
51
|
constructor(deps: ConsistencyCheckServiceDeps) {
|
|
29
52
|
this.designDocumentPort = deps.designDocumentPort;
|
|
30
53
|
this.adrReferencePort = deps.adrReferencePort;
|
|
54
|
+
this.workItemReflectionPort = deps.workItemReflectionPort;
|
|
31
55
|
}
|
|
32
56
|
|
|
33
57
|
async check(targetDocs?: readonly string[]): Promise<ConsistencyReport> {
|
|
34
58
|
const layerAnnotations = await this.designDocumentPort.getLayerAnnotations(targetDocs);
|
|
35
59
|
|
|
36
|
-
const mismatchPairs:
|
|
60
|
+
const mismatchPairs: MismatchPair[] = [];
|
|
37
61
|
const checkTargets = Object.keys(layerAnnotations);
|
|
38
62
|
|
|
39
63
|
for (const [location, annotation] of Object.entries(layerAnnotations)) {
|
|
@@ -75,4 +99,57 @@ export class ConsistencyCheckService {
|
|
|
75
99
|
checkedAt: new Date().toISOString(),
|
|
76
100
|
});
|
|
77
101
|
}
|
|
102
|
+
|
|
103
|
+
async checkWorkItemReflection(input: {
|
|
104
|
+
readonly inceptionRoot: string;
|
|
105
|
+
readonly designRoot: string;
|
|
106
|
+
}): Promise<{ readonly report: ConsistencyReport; readonly skipReason?: string }> {
|
|
107
|
+
if (!this.workItemReflectionPort) {
|
|
108
|
+
return {
|
|
109
|
+
report: ConsistencyReport.create({ mismatchPairs: [], checkTargets: [], checkedAt: new Date().toISOString() }),
|
|
110
|
+
skipReason: 'work item reflection scanner is not configured',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const snapshot = await this.workItemReflectionPort.collect(input);
|
|
115
|
+
const checkTargets = [
|
|
116
|
+
...snapshot.workItems.map((item) => `${item.path}#work-item:${item.id}`),
|
|
117
|
+
...snapshot.productRefs.map((ref) => `${ref.path}#work-item:${ref.id}`),
|
|
118
|
+
];
|
|
119
|
+
const productRefIds = new Set(snapshot.productRefs.map((ref) => ref.id));
|
|
120
|
+
const workItemIds = new Set(snapshot.workItems.map((item) => item.id));
|
|
121
|
+
const reflectionRequiredItems = snapshot.workItems.filter((item) => item.type !== 'chore');
|
|
122
|
+
const mismatchPairs: MismatchPair[] = [];
|
|
123
|
+
|
|
124
|
+
for (const item of reflectionRequiredItems) {
|
|
125
|
+
if (!productRefIds.has(item.id)) {
|
|
126
|
+
mismatchPairs.push({
|
|
127
|
+
expected: `product docs contain @work-item-id ${item.id}`,
|
|
128
|
+
actual: 'missing product reflection',
|
|
129
|
+
location: item.path,
|
|
130
|
+
nextAction: `Add @work-item-id ${item.id} to the matching product construction document under ${input.designRoot}.`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const ref of snapshot.productRefs) {
|
|
136
|
+
if (!workItemIds.has(ref.id)) {
|
|
137
|
+
mismatchPairs.push({
|
|
138
|
+
expected: `inception description exists for ${ref.id}`,
|
|
139
|
+
actual: 'orphan product reflection',
|
|
140
|
+
location: ref.path,
|
|
141
|
+
nextAction: `Create or restore an inception description for ${ref.id} under ${input.inceptionRoot}, or remove the stale product annotation.`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
report: ConsistencyReport.create({
|
|
148
|
+
mismatchPairs,
|
|
149
|
+
checkTargets,
|
|
150
|
+
checkedAt: new Date().toISOString(),
|
|
151
|
+
}),
|
|
152
|
+
skipReason: snapshot.skipReason,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
78
155
|
}
|
|
@@ -22,6 +22,7 @@ export interface ValidationResultRawProps {
|
|
|
22
22
|
readonly errors: readonly HarnessErrorLike[];
|
|
23
23
|
readonly durationMs: number;
|
|
24
24
|
readonly skipped: boolean;
|
|
25
|
+
readonly skipReason?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export class ValidationResult {
|
|
@@ -30,6 +31,7 @@ export class ValidationResult {
|
|
|
30
31
|
readonly errors: readonly HarnessErrorLike[];
|
|
31
32
|
readonly durationMs: number;
|
|
32
33
|
readonly skipped: boolean;
|
|
34
|
+
readonly skipReason?: string;
|
|
33
35
|
|
|
34
36
|
private constructor(props: ValidationResultRawProps) {
|
|
35
37
|
this.validatorId = props.validatorId;
|
|
@@ -37,6 +39,7 @@ export class ValidationResult {
|
|
|
37
39
|
this.errors = Object.freeze([...props.errors]);
|
|
38
40
|
this.durationMs = props.durationMs;
|
|
39
41
|
this.skipped = props.skipped;
|
|
42
|
+
this.skipReason = props.skipReason;
|
|
40
43
|
Object.freeze(this);
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -71,6 +74,10 @@ export class ValidationResult {
|
|
|
71
74
|
return new ValidationResult({ validatorId, passed: true, errors: [], durationMs: 0, skipped: true });
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
static skipWithReason(validatorId: ValidatorId, skipReason: string): ValidationResult {
|
|
78
|
+
return new ValidationResult({ validatorId, passed: true, errors: [], durationMs: 0, skipped: true, skipReason });
|
|
79
|
+
}
|
|
80
|
+
|
|
74
81
|
hasErrors(): boolean {
|
|
75
82
|
return this.errors.length > 0;
|
|
76
83
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer infrastructure
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-217
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import type {
|
|
9
|
+
WorkItemReflectionPort,
|
|
10
|
+
WorkItemReflectionSnapshot,
|
|
11
|
+
} from '../../domain/services/l4/consistency-check-service.js';
|
|
12
|
+
|
|
13
|
+
const DESCRIPTION_FILE = 'description.md';
|
|
14
|
+
const FRONTMATTER_PATTERN = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
15
|
+
const WORK_ITEM_ANNOTATION_PATTERN = /@work-item-id\s+([^<\r\n]+)/g;
|
|
16
|
+
|
|
17
|
+
function normalizePath(value: string): string {
|
|
18
|
+
return value.replace(/\\/g, '/');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function listFiles(root: string): Promise<string[]> {
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
24
|
+
const files: string[] = [];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const path = join(root, entry.name);
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
files.push(...(await listFiles(path)));
|
|
29
|
+
} else if (entry.isFile()) {
|
|
30
|
+
files.push(path);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractFrontmatterValue(markdown: string, key: string): string | undefined {
|
|
40
|
+
const frontmatter = FRONTMATTER_PATTERN.exec(markdown)?.[1];
|
|
41
|
+
if (!frontmatter) return undefined;
|
|
42
|
+
const pattern = new RegExp(`^${key}:\\s*(.+?)\\s*$`, 'm');
|
|
43
|
+
return pattern.exec(frontmatter)?.[1]?.replace(/^["']|["']$/g, '').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractWorkItemRefs(markdown: string): string[] {
|
|
47
|
+
const refs = new Set<string>();
|
|
48
|
+
for (const match of markdown.matchAll(WORK_ITEM_ANNOTATION_PATTERN)) {
|
|
49
|
+
const raw = match[1].replace(/-->.*/, '');
|
|
50
|
+
for (const token of raw.split(/[,\s]+/).map((part) => part.trim()).filter(Boolean)) {
|
|
51
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*-\d+(?:-\d+)*$/.test(token)) {
|
|
52
|
+
refs.add(token);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return [...refs];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class FileSystemWorkItemReflectionAdapter implements WorkItemReflectionPort {
|
|
60
|
+
constructor(private readonly projectRoot: string) {}
|
|
61
|
+
|
|
62
|
+
async collect(input: {
|
|
63
|
+
readonly inceptionRoot: string;
|
|
64
|
+
readonly designRoot: string;
|
|
65
|
+
}): Promise<WorkItemReflectionSnapshot> {
|
|
66
|
+
const inceptionRoot = join(this.projectRoot, input.inceptionRoot);
|
|
67
|
+
const designRoot = join(this.projectRoot, input.designRoot);
|
|
68
|
+
const descriptionFiles = (await listFiles(inceptionRoot))
|
|
69
|
+
.filter((path) => path.endsWith(`/${DESCRIPTION_FILE}`));
|
|
70
|
+
|
|
71
|
+
if (descriptionFiles.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
workItems: [],
|
|
74
|
+
productRefs: [],
|
|
75
|
+
skipReason: `no work item descriptions found under ${input.inceptionRoot}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const workItems: Array<WorkItemReflectionSnapshot['workItems'][number]> = [];
|
|
80
|
+
for (const path of descriptionFiles) {
|
|
81
|
+
const markdown = await readFile(path, 'utf8');
|
|
82
|
+
const id = extractFrontmatterValue(markdown, 'id');
|
|
83
|
+
if (!id) continue;
|
|
84
|
+
workItems.push({
|
|
85
|
+
id,
|
|
86
|
+
path: normalizePath(relative(this.projectRoot, path)),
|
|
87
|
+
type: extractFrontmatterValue(markdown, 'type'),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const productRefs: Array<WorkItemReflectionSnapshot['productRefs'][number]> = [];
|
|
92
|
+
const productFiles = (await listFiles(designRoot)).filter((path) => path.endsWith('.md'));
|
|
93
|
+
for (const path of productFiles) {
|
|
94
|
+
const markdown = await readFile(path, 'utf8');
|
|
95
|
+
for (const id of extractWorkItemRefs(markdown)) {
|
|
96
|
+
productRefs.push({ id, path: normalizePath(relative(this.projectRoot, path)) });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { workItems, productRefs };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -18,6 +18,9 @@ export class AgentValidationResultFormatter {
|
|
|
18
18
|
lines.push(`VALIDATOR: ${result.validatorId}`);
|
|
19
19
|
lines.push(`STATUS: ${result.skipped ? 'SKIPPED' : result.passed ? 'PASSED' : 'FAILED'}`);
|
|
20
20
|
lines.push(`DURATION: ${result.durationMs}ms`);
|
|
21
|
+
if (result.skipped && result.skipReason) {
|
|
22
|
+
lines.push(`SKIP_REASON: ${result.skipReason}`);
|
|
23
|
+
}
|
|
21
24
|
if (result.errors.length > 0) {
|
|
22
25
|
lines.push('ERRORS:');
|
|
23
26
|
for (const error of result.errors) {
|
|
@@ -25,6 +25,9 @@ export class HumanValidationResultFormatter {
|
|
|
25
25
|
? 'FAIL'
|
|
26
26
|
: 'WARN';
|
|
27
27
|
lines.push(`[${status}] ${result.validatorId} (${result.durationMs}ms)`);
|
|
28
|
+
if (result.skipped && result.skipReason) {
|
|
29
|
+
lines.push(` → ${result.skipReason}`);
|
|
30
|
+
}
|
|
28
31
|
for (const error of result.errors) {
|
|
29
32
|
lines.push(` ⚠ ${error.message}`);
|
|
30
33
|
lines.push(` → ${error.suggestion}`);
|