phasegate 0.39.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.ja.md +32 -0
  2. package/README.md +33 -0
  3. package/docs/guide/cli-reference.md +15 -0
  4. package/docs/guide/codex-integration.md +162 -0
  5. package/docs/guide/quick-vs-full-mode.md +141 -0
  6. package/package.json +1 -1
  7. package/scripts/harness/agent-integration/domain/services/bash-write-target-extractor.ts +60 -0
  8. package/scripts/harness/agent-integration/presentation/phasegate-status-context.ts +299 -0
  9. package/scripts/harness/agent-integration/presentation/session-start-hook.ts +54 -0
  10. package/scripts/harness/agent-integration/presentation/user-prompt-submit-hook.ts +70 -0
  11. package/scripts/harness/ci-governance/application/usecases/generate-ci-template-usecase.ts +6 -2
  12. package/scripts/harness/harness-api/infrastructure/adapters/validator-system-execution-adapter.ts +11 -2
  13. package/scripts/harness/integrations/pre-commit.ts +161 -127
  14. package/scripts/harness/main.ts +94 -11
  15. package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-input.ts +9 -0
  16. package/scripts/harness/phase2-extensions/application/dto/check-initial-creation-expiration-output.ts +17 -0
  17. package/scripts/harness/phase2-extensions/application/usecases/check-initial-creation-expiration-usecase.ts +103 -0
  18. package/scripts/harness/phase2-extensions/composition-root.ts +22 -0
  19. package/scripts/harness/phase2-extensions/domain/aggregates/initial-creation-expiration-rule.ts +103 -0
  20. package/scripts/harness/phase2-extensions/domain/ports/frontmatter-reader-port.ts +17 -0
  21. package/scripts/harness/phase2-extensions/domain/ports/initial-creation-age-port.ts +9 -0
  22. package/scripts/harness/phase2-extensions/domain/ports/initial-creation-expiration-config-port.ts +9 -0
  23. package/scripts/harness/phase2-extensions/domain/services/initial-creation-expiration-check-service.ts +54 -0
  24. package/scripts/harness/phase2-extensions/domain/value-objects/initial-creation-age.ts +54 -0
  25. package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-document-age-adapter.ts +7 -1
  26. package/scripts/harness/phase2-extensions/infrastructure/adapters/git-log-initial-creation-age-adapter.ts +77 -0
  27. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-initial-creation-expiration-adapter.ts +57 -0
  28. package/scripts/harness/phase2-extensions/infrastructure/adapters/markdown-frontmatter-reader-adapter.ts +56 -0
  29. package/scripts/harness/phase2-extensions/presentation/formatters/initial-creation-expiration-result-formatter.ts +23 -0
  30. package/scripts/harness/phase2-extensions/presentation/handlers/check-initial-creation-expiration-handler.ts +38 -0
  31. package/scripts/harness/quick-mode/infrastructure/adapters/git-diff-changed-files-adapter.ts +20 -1
  32. package/scripts/harness/regression-suite/composition-root.ts +4 -1
  33. package/scripts/harness/setup/skill-deployer.ts +30 -0
  34. package/scripts/harness/traceability-model/composition-root.ts +14 -0
  35. package/scripts/harness/traceability-model/domain/value-objects/project-relative-path.ts +3 -0
  36. package/scripts/harness/traceability-model/infrastructure/parsers/markdown-story-annotation-parser.ts +39 -4
  37. package/scripts/harness/traceability-model/presentation/cli/validate-metadata-command-handler.ts +103 -9
  38. package/scripts/harness/validator-system/application/dto/run-full-validation-input.ts +5 -0
  39. package/scripts/harness/validator-system/application/use-cases/run-full-validation-usecase.ts +40 -12
  40. package/scripts/harness/validator-system/composition-root.ts +2 -0
  41. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +29 -2
  42. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +17 -0
  43. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +65 -5
  44. package/scripts/harness/validator-system/presentation/handlers/run-validators-handler.ts +9 -0
  45. package/skills/domain-designer/SKILL.md +34 -0
  46. package/skills/it-test-logic-designer/SKILL.md +16 -0
  47. package/skills/logical-designer/SKILL.md +34 -0
  48. package/skills/quick-implementor/SKILL.md +10 -1
  49. package/skills/scenario-test-logic-designer/SKILL.md +16 -0
  50. package/skills/story-implementor/SKILL.md +58 -0
  51. package/skills/unit-designer/SKILL.md +41 -0
  52. package/skills/unit-test-logic-designer/SKILL.md +18 -0
  53. package/templates/.codex/hooks.json +63 -0
  54. package/templates/logical_design.template.md +79 -0
  55. package/templates/source.template.ts +18 -0
  56. package/templates/test.template.ts +37 -0
@@ -14,6 +14,8 @@ import { MetadataValidator } from './domain/services/metadata-validator.js';
14
14
  import { TraceabilityChainBuilder } from './domain/services/traceability-chain-builder.js';
15
15
  import { StoryIdAliasResolver } from './domain/services/story-id-alias-resolver.js';
16
16
  import { ValidateImplementationMetadataUseCase } from './application/usecases/validate-implementation-metadata-usecase.js';
17
+ import { ValidateDesignStoryAnnotationsUseCase } from './application/usecases/validate-design-story-annotations-usecase.js';
18
+ import { ValidateTestStoryMetadataUseCase } from './application/usecases/validate-test-story-metadata-usecase.js';
17
19
  import { ValidateMetadataCommandHandler } from './presentation/cli/validate-metadata-command-handler.js';
18
20
  import { ProjectRelativePath } from './domain/value-objects/project-relative-path.js';
19
21
 
@@ -45,10 +47,22 @@ export function createTraceabilityModelModule(rootDir: string) {
45
47
  metadataReaderPort: metadataReader,
46
48
  validator: metadataValidator,
47
49
  });
50
+ const validateDesignStoryAnnotationsUseCase =
51
+ new ValidateDesignStoryAnnotationsUseCase({
52
+ designDocumentPort: designDocument,
53
+ validator: metadataValidator,
54
+ });
55
+ const validateTestStoryMetadataUseCase =
56
+ new ValidateTestStoryMetadataUseCase({
57
+ metadataReaderPort: metadataReader,
58
+ validator: metadataValidator,
59
+ });
48
60
 
49
61
  // Presentation handlers
50
62
  const validateMetadataCommandHandler = new ValidateMetadataCommandHandler({
51
63
  validateImplementationMetadataUseCase,
64
+ validateDesignStoryAnnotationsUseCase,
65
+ validateTestStoryMetadataUseCase,
52
66
  createProjectRelativePath: (value: string) =>
53
67
  ProjectRelativePath.create(value),
54
68
  });
@@ -7,9 +7,12 @@
7
7
  const ALLOWED_PROJECT_ROOTS = new Set(['docs', 'scripts']);
8
8
 
9
9
  export class ProjectRelativePathError extends Error {
10
+ readonly value: string;
11
+
10
12
  constructor(value: string) {
11
13
  super(`ProjectRelativePathが不正です: ${value}`);
12
14
  this.name = 'ProjectRelativePathError';
15
+ this.value = value;
13
16
  }
14
17
  }
15
18
 
@@ -2,7 +2,8 @@
2
2
  * @layer infrastructure
3
3
  * @unit traceability-model
4
4
  *
5
- * Markdown本文から @story-id HXX-XX の独立行を抽出するパーサー
5
+ * Markdown本文から @story-id HXX-XX の独立行を抽出するパーサー。
6
+ * code-span (backtick) と code-fence (``` / ~~~) 内部の @story-id は prose として扱わず無視する。
6
7
  */
7
8
 
8
9
  export interface ParsedStoryAnnotation {
@@ -14,22 +15,56 @@ export interface ParsedStoryAnnotation {
14
15
 
15
16
  const STORY_ID_LINE_PATTERN = /^@story-id\s+(.+)$/;
16
17
  const STORY_ID_INLINE_PATTERN = /@story-id\s+(\S+)/;
18
+ const BACKTICK_FENCE_PATTERN = /^\s*```/;
19
+ const TILDE_FENCE_PATTERN = /^\s*~~~/;
20
+
21
+ type FenceChar = '`' | '~' | null;
22
+
23
+ function detectFenceChar(line: string): FenceChar {
24
+ if (BACKTICK_FENCE_PATTERN.test(line)) return '`';
25
+ if (TILDE_FENCE_PATTERN.test(line)) return '~';
26
+ return null;
27
+ }
28
+
29
+ function stripCodeSpans(line: string): string {
30
+ return line.replace(/`[^`\n]*`/g, '');
31
+ }
17
32
 
18
33
  /**
19
34
  * Markdown本文から @story-id 注釈を抽出する。
20
35
  * 行頭/行末空白を除去して独立行判定し、次行のコンテキストを contextLine として保持する。
36
+ * code-fence / code-span 内部の @story-id は除外する。
21
37
  */
22
38
  export function parseStoryAnnotations(
23
39
  content: string,
24
40
  ): readonly ParsedStoryAnnotation[] {
25
41
  const lines = content.split('\n');
26
42
  const annotations: ParsedStoryAnnotation[] = [];
43
+ let fenceChar: FenceChar = null;
27
44
 
28
45
  for (let i = 0; i < lines.length; i++) {
29
- const trimmedLine = lines[i].trim();
46
+ const rawLine = lines[i];
47
+ const trimmedLine = rawLine.trim();
30
48
  const lineNumber = i + 1;
31
49
 
32
- const standaloneMatch = STORY_ID_LINE_PATTERN.exec(trimmedLine);
50
+ const detected = detectFenceChar(rawLine);
51
+ if (fenceChar === null) {
52
+ if (detected !== null) {
53
+ fenceChar = detected;
54
+ }
55
+ if (fenceChar !== null) {
56
+ continue;
57
+ }
58
+ } else {
59
+ if (detected === fenceChar) {
60
+ fenceChar = null;
61
+ }
62
+ continue;
63
+ }
64
+
65
+ const sanitizedLine = stripCodeSpans(trimmedLine);
66
+
67
+ const standaloneMatch = STORY_ID_LINE_PATTERN.exec(sanitizedLine);
33
68
  if (standaloneMatch) {
34
69
  const contextLine =
35
70
  i + 1 < lines.length ? lines[i + 1].trim() : '';
@@ -42,7 +77,7 @@ export function parseStoryAnnotations(
42
77
  continue;
43
78
  }
44
79
 
45
- const inlineMatch = STORY_ID_INLINE_PATTERN.exec(trimmedLine);
80
+ const inlineMatch = STORY_ID_INLINE_PATTERN.exec(sanitizedLine);
46
81
  if (inlineMatch) {
47
82
  const contextLine =
48
83
  i + 1 < lines.length ? lines[i + 1].trim() : '';
@@ -4,8 +4,13 @@
4
4
  */
5
5
 
6
6
  import type { ValidateImplementationMetadataUseCase } from '../../application/usecases/validate-implementation-metadata-usecase.js';
7
+ import type { ValidateDesignStoryAnnotationsUseCase } from '../../application/usecases/validate-design-story-annotations-usecase.js';
8
+ import type { ValidateTestStoryMetadataUseCase } from '../../application/usecases/validate-test-story-metadata-usecase.js';
7
9
  import type { MetadataValidationOutput } from '../../application/dto/metadata-validation-output.js';
8
- import type { ProjectRelativePath } from '../../domain/value-objects/project-relative-path.js';
10
+ import {
11
+ ProjectRelativePathError,
12
+ type ProjectRelativePath,
13
+ } from '../../domain/value-objects/project-relative-path.js';
9
14
 
10
15
  export interface ValidateMetadataCommandInput {
11
16
  readonly filePaths: readonly string[];
@@ -19,21 +24,39 @@ export interface ValidateMetadataCommandOutput {
19
24
  }
20
25
 
21
26
  type PathFactory = (value: string) => ProjectRelativePath;
27
+ type ImplUseCase = Pick<ValidateImplementationMetadataUseCase, 'execute'>;
28
+ type DesignUseCase = Pick<ValidateDesignStoryAnnotationsUseCase, 'execute'>;
29
+ type TestUseCase = Pick<ValidateTestStoryMetadataUseCase, 'execute'>;
30
+
31
+ const DESIGN_DOCUMENT_EXTENSIONS: ReadonlySet<string> = new Set([
32
+ '.md',
33
+ '.mdx',
34
+ '.markdown',
35
+ ]);
36
+ const TEST_FILE_SUFFIXES = Object.freeze([
37
+ '.test.ts',
38
+ '.test.tsx',
39
+ '.spec.ts',
40
+ '.spec.tsx',
41
+ ]);
22
42
 
23
43
  export interface ValidateMetadataCommandHandlerDeps {
24
- readonly validateImplementationMetadataUseCase: Pick<
25
- ValidateImplementationMetadataUseCase,
26
- 'execute'
27
- >;
44
+ readonly validateImplementationMetadataUseCase: ImplUseCase;
45
+ readonly validateDesignStoryAnnotationsUseCase: DesignUseCase;
46
+ readonly validateTestStoryMetadataUseCase?: TestUseCase;
28
47
  readonly createProjectRelativePath: PathFactory;
29
48
  }
30
49
 
31
50
  export class ValidateMetadataCommandHandler {
32
- private readonly useCase: Pick<ValidateImplementationMetadataUseCase, 'execute'>;
51
+ private readonly implUseCase: ImplUseCase;
52
+ private readonly designUseCase: DesignUseCase;
53
+ private readonly testUseCase?: TestUseCase;
33
54
  private readonly createPath: PathFactory;
34
55
 
35
56
  constructor(deps: ValidateMetadataCommandHandlerDeps) {
36
- this.useCase = deps.validateImplementationMetadataUseCase;
57
+ this.implUseCase = deps.validateImplementationMetadataUseCase;
58
+ this.designUseCase = deps.validateDesignStoryAnnotationsUseCase;
59
+ this.testUseCase = deps.validateTestStoryMetadataUseCase;
37
60
  this.createPath = deps.createProjectRelativePath;
38
61
  }
39
62
 
@@ -50,7 +73,26 @@ export class ValidateMetadataCommandHandler {
50
73
 
51
74
  try {
52
75
  const paths = input.filePaths.map((p) => this.createPath(p));
53
- const results = await this.useCase.execute(paths);
76
+ const { designPaths, testPaths, implPaths } = this.classify(paths);
77
+
78
+ const [designResults, testResults, implResults] = await Promise.all([
79
+ designPaths.length > 0
80
+ ? this.designUseCase.execute(designPaths)
81
+ : Promise.resolve([] as readonly MetadataValidationOutput[]),
82
+ testPaths.length > 0 && this.testUseCase
83
+ ? this.testUseCase.execute(testPaths)
84
+ : Promise.resolve([] as readonly MetadataValidationOutput[]),
85
+ implPaths.length > 0
86
+ ? this.implUseCase.execute(implPaths)
87
+ : Promise.resolve([] as readonly MetadataValidationOutput[]),
88
+ ]);
89
+
90
+ const results = this.mergePreservingOrder(paths, [
91
+ ...designResults,
92
+ ...testResults,
93
+ ...implResults,
94
+ ]);
95
+
54
96
  const hasFailures = results.some((r) => !r.valid);
55
97
  const text = input.json
56
98
  ? JSON.stringify({ results }, null, 2)
@@ -61,7 +103,16 @@ export class ValidateMetadataCommandHandler {
61
103
  results,
62
104
  text,
63
105
  });
64
- } catch {
106
+ } catch (err) {
107
+ if (err instanceof ProjectRelativePathError) {
108
+ return Object.freeze({
109
+ exitCode: 2,
110
+ results: Object.freeze([]),
111
+ text:
112
+ `Error: invalid file path: "${err.value}"\n` +
113
+ ` Hint: paths must be project-relative and start with 'docs/' or 'scripts/'.`,
114
+ });
115
+ }
65
116
  return Object.freeze({
66
117
  exitCode: 2,
67
118
  results: Object.freeze([]),
@@ -70,6 +121,49 @@ export class ValidateMetadataCommandHandler {
70
121
  }
71
122
  }
72
123
 
124
+ private classify(paths: readonly ProjectRelativePath[]): {
125
+ readonly designPaths: readonly ProjectRelativePath[];
126
+ readonly testPaths: readonly ProjectRelativePath[];
127
+ readonly implPaths: readonly ProjectRelativePath[];
128
+ } {
129
+ const designPaths: ProjectRelativePath[] = [];
130
+ const testPaths: ProjectRelativePath[] = [];
131
+ const implPaths: ProjectRelativePath[] = [];
132
+ for (const path of paths) {
133
+ if (DESIGN_DOCUMENT_EXTENSIONS.has(path.extname())) {
134
+ designPaths.push(path);
135
+ } else if (this.isTestFile(path) && this.testUseCase) {
136
+ testPaths.push(path);
137
+ } else {
138
+ implPaths.push(path);
139
+ }
140
+ }
141
+ return { designPaths, testPaths, implPaths };
142
+ }
143
+
144
+ private isTestFile(path: ProjectRelativePath): boolean {
145
+ const value = path.toString();
146
+ return TEST_FILE_SUFFIXES.some((suffix) => value.endsWith(suffix));
147
+ }
148
+
149
+ private mergePreservingOrder(
150
+ orderedPaths: readonly ProjectRelativePath[],
151
+ results: readonly MetadataValidationOutput[],
152
+ ): readonly MetadataValidationOutput[] {
153
+ const byPath = new Map<string, MetadataValidationOutput>();
154
+ for (const result of results) {
155
+ byPath.set(result.filePath, result);
156
+ }
157
+ const ordered: MetadataValidationOutput[] = [];
158
+ for (const path of orderedPaths) {
159
+ const result = byPath.get(path.toString());
160
+ if (result) {
161
+ ordered.push(result);
162
+ }
163
+ }
164
+ return Object.freeze(ordered);
165
+ }
166
+
73
167
  private formatText(results: readonly MetadataValidationOutput[]): string {
74
168
  const lines: string[] = [];
75
169
  for (const r of results) {
@@ -13,4 +13,9 @@ export interface RunFullValidationInput {
13
13
  readonly failOnWarning?: boolean;
14
14
  readonly coverageReportPath?: string;
15
15
  readonly requirementMatrixPath?: string;
16
+ /**
17
+ * ISSUE-005 P1-4: 実行レイヤー絞り込み。未指定は全レイヤー実行。
18
+ * 指定された場合、`includeL4` より優先される。
19
+ */
20
+ readonly targetLayers?: readonly ('L2' | 'L3' | 'L4')[];
16
21
  }
@@ -32,22 +32,50 @@ export class RunFullValidationUseCase {
32
32
  }
33
33
 
34
34
  async execute(input: RunFullValidationInput): Promise<AggregatedValidationReport> {
35
+ // ISSUE-005 P1-4: targetLayers で絞り込み。未指定時は従来の includeL4 挙動を維持。
35
36
  const includeL4 = input.includeL4 !== false;
37
+ const defaultLayers: readonly ('L2' | 'L3' | 'L4')[] = includeL4
38
+ ? ['L2', 'L3', 'L4']
39
+ : ['L2', 'L3'];
40
+ const effectiveLayers = input.targetLayers ?? defaultLayers;
41
+ const runL2 = effectiveLayers.includes('L2');
42
+ const runL3 = effectiveLayers.includes('L3');
43
+ const runL4 = effectiveLayers.includes('L4');
36
44
 
37
- const l2Results = await this.l2UseCase.execute({
38
- targetPaths: input.targetPaths,
39
- unitName: input.unitName,
40
- currentPhase: input.currentPhase,
41
- });
45
+ type Result = {
46
+ validatorId: string;
47
+ passed: boolean;
48
+ errors: readonly {
49
+ code: string;
50
+ severity: string;
51
+ message: string;
52
+ suggestion: string;
53
+ [key: string]: unknown;
54
+ }[];
55
+ durationMs: number;
56
+ skipped?: boolean;
57
+ };
42
58
 
43
- const l3Results = await this.l3UseCase.execute({
44
- targetPaths: input.targetPaths,
45
- coverageReportPath: input.coverageReportPath,
46
- requirementMatrixPath: input.requirementMatrixPath,
47
- });
59
+ let l2Results: readonly Result[] = [];
60
+ if (runL2) {
61
+ l2Results = await this.l2UseCase.execute({
62
+ targetPaths: input.targetPaths,
63
+ unitName: input.unitName,
64
+ currentPhase: input.currentPhase,
65
+ });
66
+ }
67
+
68
+ let l3Results: readonly Result[] = [];
69
+ if (runL3) {
70
+ l3Results = await this.l3UseCase.execute({
71
+ targetPaths: input.targetPaths,
72
+ coverageReportPath: input.coverageReportPath,
73
+ requirementMatrixPath: input.requirementMatrixPath,
74
+ });
75
+ }
48
76
 
49
- let l4Results: readonly { validatorId: string; passed: boolean; errors: readonly { code: string; severity: string; message: string; suggestion: string; [key: string]: unknown }[]; durationMs: number; skipped?: boolean }[] = [];
50
- if (includeL4) {
77
+ let l4Results: readonly Result[] = [];
78
+ if (runL4) {
51
79
  l4Results = await this.l4UseCase.execute({
52
80
  targetUnits: input.targetUnits,
53
81
  });
@@ -100,6 +100,7 @@ export interface ValidatorSystemModule {
100
100
  runQuickModeUseCase: RunQuickModeUseCase;
101
101
  aggregateValidationResultsUseCase: AggregateValidationResultsUseCase;
102
102
  runFullValidationUseCase: RunFullValidationUseCase;
103
+ driftDetectionService: DriftDetectionService;
103
104
  handlers: {
104
105
  runValidators: RunValidatorsHandler;
105
106
  runQuickMode: RunQuickModeHandler;
@@ -229,6 +230,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
229
230
  runQuickModeUseCase,
230
231
  aggregateValidationResultsUseCase,
231
232
  runFullValidationUseCase,
233
+ driftDetectionService,
232
234
  handlers,
233
235
  };
234
236
  }
@@ -9,10 +9,20 @@ import { DriftReport } from '../../value-objects/drift-report.js';
9
9
 
10
10
  export interface DriftDetectionDesignDocumentPort {
11
11
  getElements(targetUnits?: readonly string[]): Promise<string[]>;
12
+ /**
13
+ * ISSUE-005 P3-9: element → unit 名のマップ。
14
+ * 実装されていれば DriftReport.unitName の解決に使われる (fallback: 'unknown')。
15
+ */
16
+ getElementUnitMap?(targetUnits?: readonly string[]): Promise<Record<string, string>>;
12
17
  }
13
18
 
14
19
  export interface DriftDetectionSourceCodeAnalyzerPort {
15
20
  getElements(targetUnits?: readonly string[]): Promise<string[]>;
21
+ /**
22
+ * ISSUE-005 P3-9: element → unit 名のマップ。
23
+ * 実装されていれば DriftReport.unitName の解決に使われる (fallback: 'unknown')。
24
+ */
25
+ getElementUnitMap?(targetUnits?: readonly string[]): Promise<Record<string, string>>;
16
26
  }
17
27
 
18
28
  export interface DriftDetectionServiceDeps {
@@ -33,6 +43,23 @@ export class DriftDetectionService {
33
43
  const designElements = await this.designDocumentPort.getElements(targetUnits);
34
44
  const codeElements = await this.sourceCodeAnalyzerPort.getElements(targetUnits);
35
45
 
46
+ // ISSUE-005 P3-9: element → unit のマップを取得し、DriftReport.unitName の解決に使う
47
+ const designUnitMap = this.designDocumentPort.getElementUnitMap
48
+ ? await this.designDocumentPort.getElementUnitMap(targetUnits)
49
+ : {};
50
+ const codeUnitMap = this.sourceCodeAnalyzerPort.getElementUnitMap
51
+ ? await this.sourceCodeAnalyzerPort.getElementUnitMap(targetUnits)
52
+ : {};
53
+
54
+ const resolveUnit = (element: string): string => {
55
+ return (
56
+ designUnitMap[element] ??
57
+ codeUnitMap[element] ??
58
+ targetUnits?.[0] ??
59
+ 'unknown'
60
+ );
61
+ };
62
+
36
63
  const designSet = new Set(designElements);
37
64
  const codeSet = new Set(codeElements);
38
65
 
@@ -44,7 +71,7 @@ export class DriftDetectionService {
44
71
  reports.push(
45
72
  DriftReport.create({
46
73
  direction: 'design→code',
47
- unitName: (targetUnits?.[0]) ?? 'unknown',
74
+ unitName: resolveUnit(element),
48
75
  element,
49
76
  description: `設計に存在するがコードに存在しない: ${element}`,
50
77
  recommendation: `${element} をコードに実装してください`,
@@ -59,7 +86,7 @@ export class DriftDetectionService {
59
86
  reports.push(
60
87
  DriftReport.create({
61
88
  direction: 'code→design',
62
- unitName: (targetUnits?.[0]) ?? 'unknown',
89
+ unitName: resolveUnit(element),
63
90
  element,
64
91
  description: `コードに存在するが設計に存在しない: ${element}`,
65
92
  recommendation: `${element} を設計文書に追記するか、コードから削除してください`,
@@ -44,6 +44,23 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
44
44
  const results = await this.analyzeExports(targetUnits);
45
45
  return results.flatMap((result) => result.exports.map((entry) => entry.name));
46
46
  }
47
+
48
+ /**
49
+ * ISSUE-005 P3-9: element 名から unit 名を引けるマップを返す。
50
+ * 同名 export が複数 unit に存在する場合は最初に見つかった unit を採用する。
51
+ */
52
+ async getElementUnitMap(targetUnits?: readonly string[]): Promise<Record<string, string>> {
53
+ const results = await this.analyzeExports(targetUnits);
54
+ const map: Record<string, string> = {};
55
+ for (const result of results) {
56
+ for (const entry of result.exports) {
57
+ if (!(entry.name in map)) {
58
+ map[entry.name] = result.unitName;
59
+ }
60
+ }
61
+ }
62
+ return map;
63
+ }
47
64
  }
48
65
 
49
66
  type ExportType = SourceAnalysisResult['exports'][number]['type'];
@@ -8,9 +8,55 @@ import type { DesignDocumentPort, StructuredDesignDoc } from '../../domain/ports
8
8
  import { readFile, readdir } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
10
 
11
- const SECTION_PATTERN = /^#{2,3}\s+(.+)/gm;
12
11
  const ADR_PATTERN = /ADR-\d{3}/g;
13
12
 
13
+ // ISSUE-005 P3-8: メタ見出し / 議論用セクションを drift 対象から除外するマーカー。
14
+ // 見出し行の直後 (同一行末 or 次の非空行) に置かれたコメントを拾う。
15
+ const SKIP_MARKER = /<!--\s*@drift-check\s*:\s*skip\s*-->/i;
16
+
17
+ // 見出し文字列のうち、デフォルトで drift 対象から外す既知のメタパターン。
18
+ // Unit 設計ドキュメントで頻出する「議論用」「自己評価」系セクション。
19
+ const DEFAULT_META_HEADING_PATTERNS = [
20
+ /engineering[- ]perspective/i,
21
+ /自己評価/,
22
+ /レビュー(観点|コメント)/,
23
+ /議論/,
24
+ /TODO|未決事項|Open\s+Questions?/i,
25
+ /変更履歴|Change\s*Log/i,
26
+ /参考文献|References?/i,
27
+ ];
28
+
29
+ function isMetaHeading(name: string): boolean {
30
+ return DEFAULT_META_HEADING_PATTERNS.some((p) => p.test(name));
31
+ }
32
+
33
+ function extractConceptNames(markdown: string): string[] {
34
+ const lines = markdown.split(/\r?\n/);
35
+ const headings: string[] = [];
36
+ const headingRegex = /^(#{2,3})\s+(.+?)\s*$/;
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const m = headingRegex.exec(lines[i]);
39
+ if (!m) continue;
40
+ const name = m[2].trim();
41
+ // 見出し行自体にスキップマーカーが付いている
42
+ if (SKIP_MARKER.test(lines[i])) continue;
43
+ // 次の非空行にスキップマーカーが付いている
44
+ // ただし、次行が新しい見出しの場合はそれ自身の注釈なので無視する
45
+ let j = i + 1;
46
+ while (j < lines.length && lines[j].trim() === '') j++;
47
+ if (
48
+ j < lines.length &&
49
+ !headingRegex.test(lines[j]) &&
50
+ SKIP_MARKER.test(lines[j])
51
+ ) continue;
52
+ // 既知のメタ見出しは暗黙的にスキップ
53
+ if (isMetaHeading(name)) continue;
54
+ // 名前から末尾のスキップマーカーを落とす (念のため)
55
+ headings.push(name.replace(SKIP_MARKER, '').trim());
56
+ }
57
+ return headings;
58
+ }
59
+
14
60
  export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
15
61
  private readonly docsRoot: string;
16
62
  private readonly cache = new Map<string, StructuredDesignDoc>();
@@ -38,10 +84,7 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
38
84
  const doc: StructuredDesignDoc = {
39
85
  unitName,
40
86
  docPath,
41
- concepts: Array.from(markdown.matchAll(SECTION_PATTERN), (match) => ({
42
- name: match[1].trim(),
43
- type: 'class',
44
- })),
87
+ concepts: extractConceptNames(markdown).map((name) => ({ name, type: 'class' })),
45
88
  layerDependencies: [],
46
89
  adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
47
90
  };
@@ -64,6 +107,23 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
64
107
  return docs.flatMap((doc) => doc.concepts.map((concept) => concept.name));
65
108
  }
66
109
 
110
+ /**
111
+ * ISSUE-005 P3-9: element 名から unit 名を引けるマップを返す。
112
+ * 同名 element が複数 unit に存在する場合は最初に見つかった unit を採用する。
113
+ */
114
+ async getElementUnitMap(targetUnits?: readonly string[]): Promise<Record<string, string>> {
115
+ const docs = await this.loadDesignDocuments(targetUnits);
116
+ const map: Record<string, string> = {};
117
+ for (const doc of docs) {
118
+ for (const concept of doc.concepts) {
119
+ if (!(concept.name in map)) {
120
+ map[concept.name] = doc.unitName;
121
+ }
122
+ }
123
+ }
124
+ return map;
125
+ }
126
+
67
127
  private async listUnitNames(): Promise<string[]> {
68
128
  try {
69
129
  const entries = await readdir(this.docsRoot, { withFileTypes: true });
@@ -110,12 +110,21 @@ export class RunValidatorsHandler {
110
110
  return { output, exitCode: l1Report.overallPassed ? 0 : 1 };
111
111
  }
112
112
 
113
+ // ISSUE-005 P1-4: args.layer を targetLayers にマップ
114
+ let targetLayers: readonly ('L2' | 'L3' | 'L4')[] | undefined;
115
+ if (args.layer === 'L2') targetLayers = ['L2'];
116
+ else if (args.layer === 'L3') targetLayers = ['L3'];
117
+ else if (args.layer === 'L4') targetLayers = ['L4'];
118
+ else if (args.layer === 'all') targetLayers = ['L2', 'L3', 'L4'];
119
+ // undefined → フィルタなし(従来挙動)
120
+
113
121
  const report = await this.useCase.execute({
114
122
  targetPaths: args.targetPaths ?? [],
115
123
  unitName: args.unit ?? '',
116
124
  currentPhase: args.phase ?? '',
117
125
  includeL4: !args.noL4,
118
126
  failOnWarning: args.failOnWarning,
127
+ targetLayers,
119
128
  });
120
129
 
121
130
  const format = args.format ?? 'human';
@@ -142,6 +142,40 @@ DDD戦術パターンを用いてドメインモデルを設計するスキル
142
142
 
143
143
  ---
144
144
 
145
+ ## 🔗 成果物のトレーサビリティメタデータ(必須)
146
+
147
+ Phase 2 で生成する設計文書には、以下 2 種類のメタデータを emit する。`MetadataValidator.validateDesignDocument` が検証対象とし、ISSUE-008 Phase B-2/B-3 完了後は `npx phasegate validate-metadata` / pre-commit で自動チェックされる。
148
+
149
+ ### 1. YAML frontmatter(新規作成時)
150
+
151
+ 文書先頭に以下を付与する。既存文書の改訂時は省略してよい。
152
+
153
+ ```yaml
154
+ ---
155
+ traceability:
156
+ initial_creation: true
157
+ ---
158
+ ```
159
+
160
+ `initial_creation: true` は「新規作成であり、後述の `@story-id` 注釈が必須」であることを示す。
161
+
162
+ ### 2. `@story-id` インライン注釈
163
+
164
+ ユーザーストーリーに紐づく集約・エンティティ・VO・ドメインイベントの直前に `@story-id HXX-XX` を独立行で記述する。
165
+
166
+ ```markdown
167
+ @story-id H03-02
168
+ ### 集約: Order(注文)
169
+ ```
170
+
171
+ 形式ルール:
172
+ - **独立行** — 他のテキストと混在させない
173
+ - **直後に設計要素** — 空行を挟まない
174
+ - **StoryCatalog 存在** — `HXX-XX` は `docs/product/user_stories.md` に存在する ID
175
+ - **複数ストーリー時** — 注釈行を連続で並べ、最後の直後に設計要素を置く
176
+
177
+ ---
178
+
145
179
  ## 注意事項
146
180
 
147
181
  - **ファイル配置は `docs/folder_management_rules.md` に従うこと**
@@ -231,6 +231,22 @@ Repositoryテストでは、`getTestClient()` でDBに直接クエリし永続
231
231
  - **WARNのみFAIL** → Opusが直接修正してから完了とする
232
232
  - **全PASS** → 完了
233
233
 
234
+ ## 🔗 テストファイルのトレーサビリティメタデータ(必須)
235
+
236
+ Phase 2 で設計する IT テストファイル(`*.test.ts` / `*.it.test.ts`)の疑似コード冒頭には、ファイル先頭コメントブロックに `// @story HXX-XX` を emit するよう明記する。`MetadataValidator.validateTest` が検証対象とし、ISSUE-008 Phase C-2 以降は `npx phasegate validate-metadata` / pre-commit で自動チェックされる。
237
+
238
+ ```typescript
239
+ // @unit <被テストコードと同じ Unit ID>
240
+ // @layer <被テストコードと同じ layer>
241
+ // @story H03-02
242
+ ```
243
+
244
+ 形式ルール:
245
+ - `@unit` / `@layer` と同じヘッダーコメントブロックに配置
246
+ - `HXX-XX` は `docs/product/user_stories.md` に存在する ID(StoryCatalog)
247
+ - 複数ストーリーをカバーする IT テストは `// @story H03-01, H03-02` のようにカンマ区切りで列挙
248
+ - 目的: US↔テストの逆引きを機械化(test-coverage-checker / nyquist の集計入力)
249
+
234
250
  ## 注意事項
235
251
 
236
252
  - **テストコードは生成しない**(設計文書のみ)— 実装は `story-implementor` スキル(codex-delegator経由、またはメインセッションで直接実行)が行う