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.
- package/CHANGELOG.md +13 -0
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/docs/guide/cli-reference.md +13 -0
- package/docs/guide/layer-model.md +4 -0
- package/package.json +1 -1
- package/scripts/harness/main.ts +16 -0
- package/scripts/harness/nyquist-validation/application/dto/generate-matrix-output.ts +71 -0
- package/scripts/harness/nyquist-validation/application/usecases/generate-requirement-test-matrix-usecase.ts +161 -0
- package/scripts/harness/nyquist-validation/composition-root.ts +24 -1
- package/scripts/harness/nyquist-validation/domain/services/requirement-intent-coverage-service.ts +42 -0
- package/scripts/harness/nyquist-validation/index.ts +9 -1
- package/scripts/harness/nyquist-validation/infrastructure/adapters/file-system-generated-matrix-adapter.ts +24 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/markdown-requirement-source-adapter.ts +37 -0
- package/scripts/harness/nyquist-validation/infrastructure/adapters/type-script-test-reference-source-adapter.ts +54 -0
- package/scripts/harness/nyquist-validation/presentation/handlers/generate-matrix-handler.ts +34 -0
- package/scripts/harness/phase2-extensions/application/dto/validate-doc-pointers-output.ts +4 -0
- package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +23 -4
- package/scripts/harness/phase2-extensions/domain/aggregates/pointer-rule.ts +13 -0
- package/scripts/harness/phase2-extensions/domain/services/freshness-check-service.ts +10 -0
- package/scripts/harness/phase2-extensions/domain/value-objects/document-age.ts +2 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +12 -0
- package/scripts/harness/phase2-extensions/presentation/formatters/pointer-result-formatter.ts +3 -1
- package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +15 -17
- package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +95 -51
- package/scripts/harness/validator-system/domain/services/l4/semantic-drift-service.ts +112 -0
- package/scripts/harness/validator-system/domain/value-objects/consistency-report.ts +2 -1
- package/scripts/harness/validator-system/domain/value-objects/semantic-drift-report.ts +50 -0
- package/scripts/harness/validator-system/index.ts +2 -0
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +30 -2
- 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);
|
package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts
CHANGED
|
@@ -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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
+
}
|