phasegate 0.150.0 → 0.150.2

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 (31) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/docs/guide/cli-reference.md +13 -0
  5. package/docs/guide/layer-model.md +4 -0
  6. package/package.json +1 -1
  7. package/scripts/harness/main.ts +16 -0
  8. package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +71 -0
  9. package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +161 -0
  10. package/scripts/harness/nyquist-validation/composition-root.ts +24 -1
  11. package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +42 -0
  12. package/scripts/harness/nyquist-validation/index.ts +9 -1
  13. package/scripts/harness/nyquist-validation/infrastructure/adapters/file-system-generated-matrix-adapter.ts +24 -0
  14. package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +37 -0
  15. package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +54 -0
  16. package/scripts/harness/nyquist-validation/presentation/handlers/generate-matrix-handler.ts +34 -0
  17. package/scripts/harness/phase2-extensions/application/dto/validate-doc-pointers-output.ts +4 -0
  18. package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +23 -4
  19. package/scripts/harness/phase2-extensions/domain/aggregates/pointer-rule.ts +13 -0
  20. package/scripts/harness/phase2-extensions/domain/services/freshness-check-service.ts +10 -0
  21. package/scripts/harness/phase2-extensions/domain/value-objects/document-age.ts +2 -2
  22. package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +12 -0
  23. package/scripts/harness/phase2-extensions/presentation/formatters/pointer-result-formatter.ts +3 -1
  24. package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +15 -17
  25. package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +95 -51
  26. package/scripts/harness/validator-system/domain/services/l4/semantic-drift-service.ts +112 -0
  27. package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +2 -1
  28. package/scripts/harness/validator-system/domain/value-objects/semantic-drift-report.ts +50 -0
  29. package/scripts/harness/validator-system/index.ts +2 -0
  30. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +30 -2
  31. package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +110 -20
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import * as ts from 'typescript';
9
9
  import type { SourceCodeAnalyzerPort, SourceAnalysisResult } from '../../domain/ports/source-code-analyzer-port.js';
10
+ import type { DriftElementRecord } from '../../domain/services/l4/drift-detection-service.js';
10
11
  import { readdir } from 'node:fs/promises';
11
12
  import { basename, join, relative, sep } from 'node:path';
12
13
 
@@ -44,7 +45,7 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
44
45
  const sourceFile = program.getSourceFile(filePath);
45
46
  if (!sourceFile) continue;
46
47
  results.push({
47
- unitName: resolveUnitName(this.sourceRoot, filePath),
48
+ unitName: resolveUnitName(this.sourceRoot, filePath, sourceFile.text),
48
49
  filePath,
49
50
  exports: extractExports(sourceFile),
50
51
  imports: extractImports(sourceFile),
@@ -86,6 +87,17 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
86
87
  }
87
88
  return map;
88
89
  }
90
+
91
+ async getElementRecords(targetUnits?: readonly string[]): Promise<readonly DriftElementRecord[]> {
92
+ const results = await this.analyzeExports(targetUnits);
93
+ return results.flatMap((result) =>
94
+ result.exports.map((entry) => ({
95
+ element: entry.name,
96
+ unitName: result.unitName,
97
+ filePaths: [result.filePath],
98
+ }))
99
+ );
100
+ }
89
101
  }
90
102
 
91
103
  type ExportType = SourceAnalysisResult['exports'][number]['type'];
@@ -112,6 +124,19 @@ function extractExports(sourceFile: ts.SourceFile): SourceAnalysisResult['export
112
124
  exports.push({ name: decl.name.text, type: 'const' });
113
125
  }
114
126
  }
127
+ } else if (ts.isExportDeclaration(node) && node.exportClause) {
128
+ if (ts.isNamedExports(node.exportClause)) {
129
+ for (const element of node.exportClause.elements) {
130
+ exports.push({ name: element.name.text, type: 'type' });
131
+ }
132
+ }
133
+ } else if (ts.isExportDeclaration(node) && !node.exportClause && ts.isStringLiteral(node.moduleSpecifier)) {
134
+ exports.push({ name: `* from ${node.moduleSpecifier.text}`, type: 'type' });
135
+ }
136
+
137
+ const hasDefault = modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
138
+ if (hasDefault) {
139
+ exports.push({ name: 'default', type: 'type' });
115
140
  }
116
141
  });
117
142
 
@@ -182,7 +207,10 @@ async function walkTsFiles(root: string, excludePattern: RegExp): Promise<string
182
207
  }
183
208
  }
184
209
 
185
- function resolveUnitName(sourceRoot: string, filePath: string): string {
210
+ function resolveUnitName(sourceRoot: string, filePath: string, sourceText: string): string {
211
+ const unitMatch = /@unit\s+([a-z0-9-]+)/i.exec(sourceText);
212
+ if (unitMatch) return unitMatch[1];
213
+
186
214
  const relativePath = relative(sourceRoot, filePath);
187
215
  const [firstSegment] = relativePath.split(sep);
188
216
  return firstSegment || basename(filePath);
@@ -5,10 +5,20 @@
5
5
  * MarkdownDesignDocumentAdapter — DesignDocumentPort実装
6
6
  */
7
7
  import type { DesignDocumentPort, StructuredDesignDoc } from '../../domain/ports/design-document-port.js';
8
+ import type { DriftElementRecord } from '../../domain/services/l4/drift-detection-service.js';
8
9
  import { readFile, readdir } from 'node:fs/promises';
9
10
  import { join } from 'node:path';
10
11
 
11
12
  const ADR_PATTERN = /ADR-\d{3}/g;
13
+ const WORK_ITEM_PATTERN = /@work-item-id\s+(WI-\d{3})/g;
14
+ const LAYER_PATTERN = /@layer\s+([a-z0-9-]+)/gi;
15
+ const UNIT_PATTERN = /@unit\s+([a-z0-9-]+)/gi;
16
+ const CONSTRUCTION_DOC_NAMES = [
17
+ 'domain_model.md',
18
+ 'logical_design.md',
19
+ 'unit_test_design.md',
20
+ 'it_test_design.md',
21
+ ];
12
22
 
13
23
  // ISSUE-005 P3-8: メタ見出し / 議論用セクションを drift 対象から除外するマーカー。
14
24
  // 見出し行の直後 (同一行末 or 次の非空行) に置かれたコメントを拾う。
@@ -111,26 +121,28 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
111
121
  const results: StructuredDesignDoc[] = [];
112
122
 
113
123
  for (const unitName of unitNames) {
114
- const docPath = join(this.docsRoot, unitName, 'domain_model.md');
115
- const cached = this.cache.get(docPath);
116
- if (cached) {
117
- results.push(cached);
118
- continue;
119
- }
124
+ for (const docName of CONSTRUCTION_DOC_NAMES) {
125
+ const docPath = join(this.docsRoot, unitName, docName);
126
+ const cached = this.cache.get(docPath);
127
+ if (cached) {
128
+ results.push(cached);
129
+ continue;
130
+ }
120
131
 
121
- try {
122
- const markdown = await readFile(docPath, 'utf8');
123
- const doc: StructuredDesignDoc = {
124
- unitName,
125
- docPath,
126
- concepts: extractConcepts(markdown).map((concept) => ({ ...concept, type: 'class' })),
127
- layerDependencies: [],
128
- adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
129
- };
130
- this.cache.set(docPath, doc);
131
- results.push(doc);
132
- } catch {
133
- continue;
132
+ try {
133
+ const markdown = await readFile(docPath, 'utf8');
134
+ const doc: StructuredDesignDoc = {
135
+ unitName,
136
+ docPath,
137
+ concepts: extractConcepts(markdown).map((concept) => ({ ...concept, type: 'class' })),
138
+ layerDependencies: extractLayerDependencies(markdown),
139
+ adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
140
+ };
141
+ this.cache.set(docPath, doc);
142
+ results.push(doc);
143
+ } catch {
144
+ continue;
145
+ }
134
146
  }
135
147
  }
136
148
 
@@ -138,7 +150,32 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
138
150
  }
139
151
 
140
152
  async getLayerAnnotations(targetDocs?: readonly string[]): Promise<Record<string, string>> {
141
- return {};
153
+ const docs = targetDocs && targetDocs.length > 0
154
+ ? await this.loadExplicitDocs(targetDocs)
155
+ : await this.loadDesignDocuments();
156
+ const annotations: Record<string, string> = {};
157
+
158
+ for (const doc of docs) {
159
+ const markdown = await readFile(doc.docPath, 'utf8');
160
+ const layers = Array.from(new Set(Array.from(markdown.matchAll(LAYER_PATTERN)).map((match) => match[1])));
161
+ const units = Array.from(new Set(Array.from(markdown.matchAll(UNIT_PATTERN)).map((match) => match[1])));
162
+ const workItems = Array.from(new Set(Array.from(markdown.matchAll(WORK_ITEM_PATTERN)).map((match) => match[1])));
163
+
164
+ for (const layer of layers) {
165
+ annotations[`${doc.docPath}#layer:${layer}`] = isKnownLayer(layer) ? 'layer:known' : 'layer:unknown';
166
+ }
167
+ for (const unit of units) {
168
+ annotations[`${doc.docPath}#unit:${unit}`] = unit === doc.unitName ? 'unit:matched' : `unit:mismatch:${doc.unitName}`;
169
+ }
170
+ for (const adrRef of doc.adrRefs) {
171
+ annotations[adrRef] = 'adr:referenced';
172
+ }
173
+ for (const workItemId of workItems) {
174
+ annotations[`${doc.docPath}#work-item:${workItemId}`] = 'work-item:referenced';
175
+ }
176
+ }
177
+
178
+ return annotations;
142
179
  }
143
180
 
144
181
  async getElements(targetUnits?: readonly string[]): Promise<string[]> {
@@ -146,6 +183,17 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
146
183
  return docs.flatMap((doc) => doc.concepts.map((concept) => concept.name));
147
184
  }
148
185
 
186
+ async getElementRecords(targetUnits?: readonly string[]): Promise<readonly DriftElementRecord[]> {
187
+ const docs = await this.loadDesignDocuments(targetUnits);
188
+ return docs.flatMap((doc) =>
189
+ doc.concepts.map((concept) => ({
190
+ element: concept.name,
191
+ unitName: doc.unitName,
192
+ pointers: concept.pointers ?? [],
193
+ }))
194
+ );
195
+ }
196
+
149
197
  async getElementPointers(targetUnits?: readonly string[]): Promise<Record<string, readonly string[]>> {
150
198
  const docs = await this.loadDesignDocuments(targetUnits);
151
199
  const map: Record<string, readonly string[]> = {};
@@ -176,6 +224,26 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
176
224
  return map;
177
225
  }
178
226
 
227
+ private async loadExplicitDocs(targetDocs: readonly string[]): Promise<readonly StructuredDesignDoc[]> {
228
+ const docs: StructuredDesignDoc[] = [];
229
+ for (const docPath of targetDocs) {
230
+ try {
231
+ const markdown = await readFile(docPath, 'utf8');
232
+ const unitName = inferUnitNameFromDocPath(this.docsRoot, docPath);
233
+ docs.push({
234
+ unitName,
235
+ docPath,
236
+ concepts: extractConcepts(markdown).map((concept) => ({ ...concept, type: 'class' })),
237
+ layerDependencies: extractLayerDependencies(markdown),
238
+ adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
239
+ });
240
+ } catch {
241
+ continue;
242
+ }
243
+ }
244
+ return docs;
245
+ }
246
+
179
247
  private async listUnitNames(): Promise<string[]> {
180
248
  try {
181
249
  const entries = await readdir(this.docsRoot, { withFileTypes: true });
@@ -185,3 +253,25 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
185
253
  }
186
254
  }
187
255
  }
256
+
257
+ function extractLayerDependencies(markdown: string): Array<{ from: string; to: string }> {
258
+ const dependencies: Array<{ from: string; to: string }> = [];
259
+ const dependencyPattern = /([a-z0-9-]+)\s*(?:->|→)\s*([a-z0-9-]+)/gi;
260
+ for (const match of markdown.matchAll(dependencyPattern)) {
261
+ dependencies.push({ from: match[1], to: match[2] });
262
+ }
263
+ return dependencies;
264
+ }
265
+
266
+ function isKnownLayer(layer: string): boolean {
267
+ return ['domain', 'application', 'infrastructure', 'presentation', 'test'].includes(layer);
268
+ }
269
+
270
+ function inferUnitNameFromDocPath(docsRoot: string, docPath: string): string {
271
+ const normalizedRoot = docsRoot.replace(/\\/g, '/').replace(/\/+$/, '');
272
+ const normalizedPath = docPath.replace(/\\/g, '/');
273
+ const relativePath = normalizedPath.startsWith(`${normalizedRoot}/`)
274
+ ? normalizedPath.slice(normalizedRoot.length + 1)
275
+ : normalizedPath;
276
+ return relativePath.split('/')[0] || 'unknown';
277
+ }