phasegate 0.150.1 → 0.151.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.
- package/CHANGELOG.md +12 -0
- package/docs/guide/layer-model.md +4 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +1 -1
- package/scripts/harness/harness-api/domain/value-objects/harness-status-summary.ts +1 -0
- package/scripts/harness/phase2-extensions/application/dto/validate-doc-pointers-output.ts +5 -0
- package/scripts/harness/phase2-extensions/application/usecases/validate-doc-pointers-usecase.ts +24 -4
- package/scripts/harness/phase2-extensions/domain/aggregates/pointer-rule.ts +14 -0
- package/scripts/harness/phase2-extensions/domain/services/freshness-check-service.ts +11 -0
- package/scripts/harness/phase2-extensions/domain/value-objects/document-age.ts +3 -2
- package/scripts/harness/phase2-extensions/infrastructure/adapters/harness-config-freshness-adapter.ts +13 -0
- package/scripts/harness/phase2-extensions/presentation/formatters/pointer-result-formatter.ts +4 -1
- package/scripts/harness/validator-system/application/use-cases/run-l2-validators-usecase.ts +29 -1
- package/scripts/harness/validator-system/composition-root.ts +6 -2
- package/scripts/harness/validator-system/domain/ports/contract-traceability-policy-port.ts +9 -0
- package/scripts/harness/validator-system/domain/services/contract-traceability-coverage-service.ts +259 -0
- package/scripts/harness/validator-system/domain/services/l4/consistency-check-service.ts +16 -17
- package/scripts/harness/validator-system/domain/services/l4/drift-detection-service.ts +96 -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 +3 -1
- package/scripts/harness/validator-system/domain/value-objects/contract-traceability-model.ts +124 -0
- package/scripts/harness/validator-system/domain/value-objects/semantic-drift-report.ts +50 -0
- package/scripts/harness/validator-system/domain/value-objects/validator-id.ts +2 -0
- package/scripts/harness/validator-system/index.ts +2 -0
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +31 -2
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-contract-traceability-policy-adapter.ts +115 -0
- package/scripts/harness/validator-system/infrastructure/adapters/harness-config-validator-config-adapter.ts +1 -1
- package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts +111 -20
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// @unit validator-system
|
|
2
|
+
// @layer domain
|
|
3
|
+
// @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
|
|
4
|
+
|
|
5
|
+
export type PublicContractKind =
|
|
6
|
+
| 'cli-command'
|
|
7
|
+
| 'api-endpoint'
|
|
8
|
+
| 'port'
|
|
9
|
+
| 'config-option'
|
|
10
|
+
| 'domain-behavior'
|
|
11
|
+
| 'error-code';
|
|
12
|
+
|
|
13
|
+
export type BoundaryCaseKind =
|
|
14
|
+
| 'empty-input'
|
|
15
|
+
| 'missing-required'
|
|
16
|
+
| 'invalid-enum'
|
|
17
|
+
| 'duplicate-id'
|
|
18
|
+
| 'unknown-reference'
|
|
19
|
+
| 'permission-denied'
|
|
20
|
+
| 'config-disabled'
|
|
21
|
+
| 'partial-failure'
|
|
22
|
+
| 'idempotency'
|
|
23
|
+
| 'backward-compatibility';
|
|
24
|
+
|
|
25
|
+
export interface PublicContract {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly kind: PublicContractKind;
|
|
28
|
+
readonly sourcePath: string;
|
|
29
|
+
readonly requiredBehaviors: readonly string[];
|
|
30
|
+
readonly boundaryCases?: readonly BoundaryCaseKind[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TestObservation {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly kind: 'unit' | 'integration' | 'e2e' | 'adapter-contract';
|
|
36
|
+
readonly sourcePath: string;
|
|
37
|
+
readonly covers: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ErrorContract {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly sourcePath: string;
|
|
43
|
+
readonly code?: string;
|
|
44
|
+
readonly severity?: 'error' | 'warning';
|
|
45
|
+
readonly message?: string;
|
|
46
|
+
readonly suggestion?: string;
|
|
47
|
+
readonly documentationRef?: string;
|
|
48
|
+
readonly exitCode?: number;
|
|
49
|
+
readonly machineFields?: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface StateTransition {
|
|
53
|
+
readonly from: string;
|
|
54
|
+
readonly to: string;
|
|
55
|
+
readonly guard?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface StateMachineModel {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly sourcePath: string;
|
|
61
|
+
readonly docsStates: readonly string[];
|
|
62
|
+
readonly codeStates: readonly string[];
|
|
63
|
+
readonly transitions: readonly StateTransition[];
|
|
64
|
+
readonly terminalStates: readonly string[];
|
|
65
|
+
readonly invalidTransitions: readonly StateTransition[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TraceabilityGraphSlice {
|
|
69
|
+
readonly workItemId: string;
|
|
70
|
+
readonly affectedUnits: readonly string[];
|
|
71
|
+
readonly productUnits: readonly string[];
|
|
72
|
+
readonly implementationWorkItemIds: readonly string[];
|
|
73
|
+
readonly testWorkItemIds: readonly string[];
|
|
74
|
+
readonly publicDocsChanged: boolean;
|
|
75
|
+
readonly contractChanged: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ContractTraceabilityInput {
|
|
79
|
+
readonly publicContracts: readonly PublicContract[];
|
|
80
|
+
readonly testObservations: readonly TestObservation[];
|
|
81
|
+
readonly errorContracts: readonly ErrorContract[];
|
|
82
|
+
readonly stateMachines: readonly StateMachineModel[];
|
|
83
|
+
readonly traceabilitySlices: readonly TraceabilityGraphSlice[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ContractTraceabilityFindingKind =
|
|
87
|
+
| 'missing-required-behavior-test'
|
|
88
|
+
| 'missing-port-contract-test'
|
|
89
|
+
| 'missing-boundary-test'
|
|
90
|
+
| 'error-contract-shape'
|
|
91
|
+
| 'error-contract-exit-code'
|
|
92
|
+
| 'missing-error-path-test'
|
|
93
|
+
| 'state-doc-code-mismatch'
|
|
94
|
+
| 'state-invalid-terminal-transition'
|
|
95
|
+
| 'missing-transition-test'
|
|
96
|
+
| 'traceability-unit-mismatch'
|
|
97
|
+
| 'traceability-test-mismatch'
|
|
98
|
+
| 'public-doc-contract-sync';
|
|
99
|
+
|
|
100
|
+
export interface ContractTraceabilityFinding {
|
|
101
|
+
readonly kind: ContractTraceabilityFindingKind;
|
|
102
|
+
readonly severity: 'error' | 'warning';
|
|
103
|
+
readonly subject: string;
|
|
104
|
+
readonly sourcePath: string;
|
|
105
|
+
readonly message: string;
|
|
106
|
+
readonly suggestion: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class ContractTraceabilityReport {
|
|
110
|
+
readonly findings: readonly ContractTraceabilityFinding[];
|
|
111
|
+
|
|
112
|
+
private constructor(findings: readonly ContractTraceabilityFinding[]) {
|
|
113
|
+
this.findings = Object.freeze([...findings]);
|
|
114
|
+
Object.freeze(this);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static create(findings: readonly ContractTraceabilityFinding[]): ContractTraceabilityReport {
|
|
118
|
+
return new ContractTraceabilityReport(findings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hasFindings(): boolean {
|
|
122
|
+
return this.findings.length > 0;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-139
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type SemanticDriftKind =
|
|
8
|
+
| 'design-behavior-missing-code'
|
|
9
|
+
| 'design-behavior-missing-test'
|
|
10
|
+
| 'code-behavior-missing-design'
|
|
11
|
+
| 'code-behavior-missing-test'
|
|
12
|
+
| 'test-observation-missing-design';
|
|
13
|
+
|
|
14
|
+
export interface SemanticDriftReportProps {
|
|
15
|
+
readonly kind: SemanticDriftKind;
|
|
16
|
+
readonly behaviorId: string;
|
|
17
|
+
readonly unitName: string;
|
|
18
|
+
readonly severity: 'warning' | 'error';
|
|
19
|
+
readonly location?: string;
|
|
20
|
+
readonly nextAction: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SemanticDriftReport {
|
|
24
|
+
readonly kind: SemanticDriftKind;
|
|
25
|
+
readonly behaviorId: string;
|
|
26
|
+
readonly unitName: string;
|
|
27
|
+
readonly severity: 'warning' | 'error';
|
|
28
|
+
readonly location: string | null;
|
|
29
|
+
readonly nextAction: string;
|
|
30
|
+
|
|
31
|
+
private constructor(props: SemanticDriftReportProps) {
|
|
32
|
+
this.kind = props.kind;
|
|
33
|
+
this.behaviorId = props.behaviorId;
|
|
34
|
+
this.unitName = props.unitName;
|
|
35
|
+
this.severity = props.severity;
|
|
36
|
+
this.location = props.location ?? null;
|
|
37
|
+
this.nextAction = props.nextAction;
|
|
38
|
+
Object.freeze(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static create(props: SemanticDriftReportProps): SemanticDriftReport {
|
|
42
|
+
if (props.behaviorId.trim().length === 0) {
|
|
43
|
+
throw new Error('behaviorId is required for semantic drift reports');
|
|
44
|
+
}
|
|
45
|
+
if (props.unitName.trim().length === 0) {
|
|
46
|
+
throw new Error('unitName is required for semantic drift reports');
|
|
47
|
+
}
|
|
48
|
+
return new SemanticDriftReport(props);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* L1-001〜L4-005 のバリデータを識別する不変値オブジェクト
|
|
8
8
|
* Wave 2A で L1-017, L1-018, L2-013 を追加
|
|
9
9
|
* WI-140 で L2-014 を追加
|
|
10
|
+
* WI-132/WI-133/WI-136/WI-137/WI-138 で L2-015 を追加
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
export class InvalidValidatorIdError extends Error {
|
|
@@ -29,6 +30,7 @@ const VALIDATOR_NAME_MAP: Record<string, string> = {
|
|
|
29
30
|
'L2-003': 'test-quality',
|
|
30
31
|
'L2-013': 'cli-e2e-test-existence',
|
|
31
32
|
'L2-014': 'work-item-status-staleness',
|
|
33
|
+
'L2-015': 'contract-traceability-coverage',
|
|
32
34
|
'L3-001': 'security',
|
|
33
35
|
'L3-002': 'performance',
|
|
34
36
|
'L3-003': 'coverage',
|
|
@@ -18,6 +18,7 @@ export { LayerConfig } from './domain/value-objects/layer-config.js';
|
|
|
18
18
|
export { DriftReport } from './domain/value-objects/drift-report.js';
|
|
19
19
|
export { ConsistencyReport } from './domain/value-objects/consistency-report.js';
|
|
20
20
|
export { DeadCodeReport } from './domain/value-objects/dead-code-report.js';
|
|
21
|
+
export { SemanticDriftReport } from './domain/value-objects/semantic-drift-report.js';
|
|
21
22
|
|
|
22
23
|
// --- Domain Services ---
|
|
23
24
|
export { ValidatorRegistry, UnknownValidatorError } from './domain/services/validator-registry.js';
|
|
@@ -25,6 +26,7 @@ export { ValidatorExecutionService, ValidatorExecutionError } from './domain/ser
|
|
|
25
26
|
export { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
|
|
26
27
|
export { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
|
|
27
28
|
export { DeadCodeDetectionService } from './domain/services/l4/dead-code-detection-service.js';
|
|
29
|
+
export { SemanticDriftService } from './domain/services/l4/semantic-drift-service.js';
|
|
28
30
|
|
|
29
31
|
// --- Application DTOs ---
|
|
30
32
|
export type { ValidationResultContract } from './application/dto/validation-result-contract.js';
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-117
|
|
4
5
|
*
|
|
5
6
|
* BiomeAstSourceCodeAnalyzerAdapter — SourceCodeAnalyzerPort実装
|
|
6
7
|
* TypeScript Compiler API を使用してエクスポートを正確に抽出する(L4-001, L4-003)
|
|
7
8
|
*/
|
|
8
9
|
import * as ts from 'typescript';
|
|
9
10
|
import type { SourceCodeAnalyzerPort, SourceAnalysisResult } from '../../domain/ports/source-code-analyzer-port.js';
|
|
11
|
+
import type { DriftElementRecord } from '../../domain/services/l4/drift-detection-service.js';
|
|
10
12
|
import { readdir } from 'node:fs/promises';
|
|
11
13
|
import { basename, join, relative, sep } from 'node:path';
|
|
12
14
|
|
|
@@ -44,7 +46,7 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
|
|
|
44
46
|
const sourceFile = program.getSourceFile(filePath);
|
|
45
47
|
if (!sourceFile) continue;
|
|
46
48
|
results.push({
|
|
47
|
-
unitName: resolveUnitName(this.sourceRoot, filePath),
|
|
49
|
+
unitName: resolveUnitName(this.sourceRoot, filePath, sourceFile.text),
|
|
48
50
|
filePath,
|
|
49
51
|
exports: extractExports(sourceFile),
|
|
50
52
|
imports: extractImports(sourceFile),
|
|
@@ -86,6 +88,17 @@ export class BiomeAstSourceCodeAnalyzerAdapter implements SourceCodeAnalyzerPort
|
|
|
86
88
|
}
|
|
87
89
|
return map;
|
|
88
90
|
}
|
|
91
|
+
|
|
92
|
+
async getElementRecords(targetUnits?: readonly string[]): Promise<readonly DriftElementRecord[]> {
|
|
93
|
+
const results = await this.analyzeExports(targetUnits);
|
|
94
|
+
return results.flatMap((result) =>
|
|
95
|
+
result.exports.map((entry) => ({
|
|
96
|
+
element: entry.name,
|
|
97
|
+
unitName: result.unitName,
|
|
98
|
+
filePaths: [result.filePath],
|
|
99
|
+
}))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
type ExportType = SourceAnalysisResult['exports'][number]['type'];
|
|
@@ -112,6 +125,19 @@ function extractExports(sourceFile: ts.SourceFile): SourceAnalysisResult['export
|
|
|
112
125
|
exports.push({ name: decl.name.text, type: 'const' });
|
|
113
126
|
}
|
|
114
127
|
}
|
|
128
|
+
} else if (ts.isExportDeclaration(node) && node.exportClause) {
|
|
129
|
+
if (ts.isNamedExports(node.exportClause)) {
|
|
130
|
+
for (const element of node.exportClause.elements) {
|
|
131
|
+
exports.push({ name: element.name.text, type: 'type' });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} else if (ts.isExportDeclaration(node) && !node.exportClause && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
135
|
+
exports.push({ name: `* from ${node.moduleSpecifier.text}`, type: 'type' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hasDefault = modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
|
|
139
|
+
if (hasDefault) {
|
|
140
|
+
exports.push({ name: 'default', type: 'type' });
|
|
115
141
|
}
|
|
116
142
|
});
|
|
117
143
|
|
|
@@ -182,7 +208,10 @@ async function walkTsFiles(root: string, excludePattern: RegExp): Promise<string
|
|
|
182
208
|
}
|
|
183
209
|
}
|
|
184
210
|
|
|
185
|
-
function resolveUnitName(sourceRoot: string, filePath: string): string {
|
|
211
|
+
function resolveUnitName(sourceRoot: string, filePath: string, sourceText: string): string {
|
|
212
|
+
const unitMatch = /@unit\s+([a-z0-9-]+)/i.exec(sourceText);
|
|
213
|
+
if (unitMatch) return unitMatch[1];
|
|
214
|
+
|
|
186
215
|
const relativePath = relative(sourceRoot, filePath);
|
|
187
216
|
const [firstSegment] = relativePath.split(sep);
|
|
188
217
|
return firstSegment || basename(filePath);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// @unit validator-system
|
|
2
|
+
// @layer infrastructure
|
|
3
|
+
// @work-item-id WI-132 / WI-133 / WI-136 / WI-137 / WI-138
|
|
4
|
+
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import type { ContractTraceabilityPolicyPort } from '../../domain/ports/contract-traceability-policy-port.js';
|
|
7
|
+
import type {
|
|
8
|
+
BoundaryCaseKind,
|
|
9
|
+
ContractTraceabilityInput,
|
|
10
|
+
PublicContract,
|
|
11
|
+
PublicContractKind,
|
|
12
|
+
TestObservation,
|
|
13
|
+
} from '../../domain/value-objects/contract-traceability-model.js';
|
|
14
|
+
|
|
15
|
+
const CONTRACT_KINDS = new Set<PublicContractKind>([
|
|
16
|
+
'cli-command',
|
|
17
|
+
'api-endpoint',
|
|
18
|
+
'port',
|
|
19
|
+
'config-option',
|
|
20
|
+
'domain-behavior',
|
|
21
|
+
'error-code',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const BOUNDARY_KINDS = new Set<BoundaryCaseKind>([
|
|
25
|
+
'empty-input',
|
|
26
|
+
'missing-required',
|
|
27
|
+
'invalid-enum',
|
|
28
|
+
'duplicate-id',
|
|
29
|
+
'unknown-reference',
|
|
30
|
+
'permission-denied',
|
|
31
|
+
'config-disabled',
|
|
32
|
+
'partial-failure',
|
|
33
|
+
'idempotency',
|
|
34
|
+
'backward-compatibility',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export class FileSystemContractTraceabilityPolicyAdapter implements ContractTraceabilityPolicyPort {
|
|
38
|
+
async collect(targetPaths: readonly string[]): Promise<ContractTraceabilityInput> {
|
|
39
|
+
const publicContracts: PublicContract[] = [];
|
|
40
|
+
const testObservations: TestObservation[] = [];
|
|
41
|
+
|
|
42
|
+
for (const targetPath of targetPaths) {
|
|
43
|
+
let content: string;
|
|
44
|
+
try {
|
|
45
|
+
content = await readFile(targetPath, 'utf-8');
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
publicContracts.push(...this.extractContracts(targetPath, content));
|
|
51
|
+
testObservations.push(...this.extractObservations(targetPath, content));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
publicContracts,
|
|
56
|
+
testObservations,
|
|
57
|
+
errorContracts: [],
|
|
58
|
+
stateMachines: [],
|
|
59
|
+
traceabilitySlices: [],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private extractContracts(sourcePath: string, content: string): readonly PublicContract[] {
|
|
64
|
+
return [...content.matchAll(/@phasegate-contract\s+([^\n\r*]+)/g)].flatMap((match) => {
|
|
65
|
+
const attrs = this.parseAttrs(match[1]);
|
|
66
|
+
const id = attrs.get('id');
|
|
67
|
+
const kind = attrs.get('kind');
|
|
68
|
+
if (!id || !this.isContractKind(kind)) return [];
|
|
69
|
+
|
|
70
|
+
return [{
|
|
71
|
+
id,
|
|
72
|
+
kind,
|
|
73
|
+
sourcePath,
|
|
74
|
+
requiredBehaviors: this.splitList(attrs.get('behaviors')),
|
|
75
|
+
boundaryCases: this.splitList(attrs.get('boundary')).filter(this.isBoundaryKind),
|
|
76
|
+
}];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private extractObservations(sourcePath: string, content: string): readonly TestObservation[] {
|
|
81
|
+
return [...content.matchAll(/@phasegate-observation\s+([^\n\r*]+)/g)].flatMap((match, index) => {
|
|
82
|
+
const attrs = this.parseAttrs(match[1]);
|
|
83
|
+
const covers = this.splitList(attrs.get('covers'));
|
|
84
|
+
if (covers.length === 0) return [];
|
|
85
|
+
const kind = attrs.get('kind');
|
|
86
|
+
return [{
|
|
87
|
+
id: attrs.get('id') ?? `${sourcePath}#observation-${index + 1}`,
|
|
88
|
+
kind: kind === 'adapter-contract' || kind === 'integration' || kind === 'e2e' ? kind : 'unit',
|
|
89
|
+
sourcePath,
|
|
90
|
+
covers,
|
|
91
|
+
}];
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private parseAttrs(raw: string): Map<string, string> {
|
|
96
|
+
const attrs = new Map<string, string>();
|
|
97
|
+
for (const match of raw.matchAll(/([a-zA-Z][a-zA-Z0-9_-]*)=("[^"]*"|'[^']*'|[^\s]+)/g)) {
|
|
98
|
+
attrs.set(match[1], match[2].replace(/^['"]|['"]$/g, ''));
|
|
99
|
+
}
|
|
100
|
+
return attrs;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private splitList(raw: string | undefined): string[] {
|
|
104
|
+
if (!raw) return [];
|
|
105
|
+
return raw.split(',').map((value) => value.trim()).filter((value) => value.length > 0);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private isContractKind(value: string | undefined): value is PublicContractKind {
|
|
109
|
+
return value !== undefined && CONTRACT_KINDS.has(value as PublicContractKind);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private isBoundaryKind(value: string): value is BoundaryCaseKind {
|
|
113
|
+
return BOUNDARY_KINDS.has(value as BoundaryCaseKind);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -38,7 +38,7 @@ export class HarnessConfigValidatorConfigAdapter implements ValidatorConfigPort
|
|
|
38
38
|
const layerData = this.config.layers?.[layer] ?? {};
|
|
39
39
|
|
|
40
40
|
const defaultValidators: Record<string, string[]> = {
|
|
41
|
-
L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014'],
|
|
41
|
+
L2: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'],
|
|
42
42
|
L3: ['L3-001', 'L3-002', 'L3-003', 'L3-004'],
|
|
43
43
|
L4: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'],
|
|
44
44
|
};
|
package/scripts/harness/validator-system/infrastructure/adapters/markdown-design-document-adapter.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer infrastructure
|
|
3
3
|
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-117, WI-118
|
|
4
5
|
*
|
|
5
6
|
* MarkdownDesignDocumentAdapter — DesignDocumentPort実装
|
|
6
7
|
*/
|
|
7
8
|
import type { DesignDocumentPort, StructuredDesignDoc } from '../../domain/ports/design-document-port.js';
|
|
9
|
+
import type { DriftElementRecord } from '../../domain/services/l4/drift-detection-service.js';
|
|
8
10
|
import { readFile, readdir } from 'node:fs/promises';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
|
|
11
13
|
const ADR_PATTERN = /ADR-\d{3}/g;
|
|
14
|
+
const WORK_ITEM_PATTERN = /@work-item-id\s+(WI-\d{3})/g;
|
|
15
|
+
const LAYER_PATTERN = /@layer\s+([a-z0-9-]+)/gi;
|
|
16
|
+
const UNIT_PATTERN = /@unit\s+([a-z0-9-]+)/gi;
|
|
17
|
+
const CONSTRUCTION_DOC_NAMES = [
|
|
18
|
+
'domain_model.md',
|
|
19
|
+
'logical_design.md',
|
|
20
|
+
'unit_test_design.md',
|
|
21
|
+
'it_test_design.md',
|
|
22
|
+
];
|
|
12
23
|
|
|
13
24
|
// ISSUE-005 P3-8: メタ見出し / 議論用セクションを drift 対象から除外するマーカー。
|
|
14
25
|
// 見出し行の直後 (同一行末 or 次の非空行) に置かれたコメントを拾う。
|
|
@@ -111,26 +122,28 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
111
122
|
const results: StructuredDesignDoc[] = [];
|
|
112
123
|
|
|
113
124
|
for (const unitName of unitNames) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
for (const docName of CONSTRUCTION_DOC_NAMES) {
|
|
126
|
+
const docPath = join(this.docsRoot, unitName, docName);
|
|
127
|
+
const cached = this.cache.get(docPath);
|
|
128
|
+
if (cached) {
|
|
129
|
+
results.push(cached);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
try {
|
|
134
|
+
const markdown = await readFile(docPath, 'utf8');
|
|
135
|
+
const doc: StructuredDesignDoc = {
|
|
136
|
+
unitName,
|
|
137
|
+
docPath,
|
|
138
|
+
concepts: extractConcepts(markdown).map((concept) => ({ ...concept, type: 'class' })),
|
|
139
|
+
layerDependencies: extractLayerDependencies(markdown),
|
|
140
|
+
adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
|
|
141
|
+
};
|
|
142
|
+
this.cache.set(docPath, doc);
|
|
143
|
+
results.push(doc);
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
|
|
@@ -138,7 +151,32 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
async getLayerAnnotations(targetDocs?: readonly string[]): Promise<Record<string, string>> {
|
|
141
|
-
|
|
154
|
+
const docs = targetDocs && targetDocs.length > 0
|
|
155
|
+
? await this.loadExplicitDocs(targetDocs)
|
|
156
|
+
: await this.loadDesignDocuments();
|
|
157
|
+
const annotations: Record<string, string> = {};
|
|
158
|
+
|
|
159
|
+
for (const doc of docs) {
|
|
160
|
+
const markdown = await readFile(doc.docPath, 'utf8');
|
|
161
|
+
const layers = Array.from(new Set(Array.from(markdown.matchAll(LAYER_PATTERN)).map((match) => match[1])));
|
|
162
|
+
const units = Array.from(new Set(Array.from(markdown.matchAll(UNIT_PATTERN)).map((match) => match[1])));
|
|
163
|
+
const workItems = Array.from(new Set(Array.from(markdown.matchAll(WORK_ITEM_PATTERN)).map((match) => match[1])));
|
|
164
|
+
|
|
165
|
+
for (const layer of layers) {
|
|
166
|
+
annotations[`${doc.docPath}#layer:${layer}`] = isKnownLayer(layer) ? 'layer:known' : 'layer:unknown';
|
|
167
|
+
}
|
|
168
|
+
for (const unit of units) {
|
|
169
|
+
annotations[`${doc.docPath}#unit:${unit}`] = unit === doc.unitName ? 'unit:matched' : `unit:mismatch:${doc.unitName}`;
|
|
170
|
+
}
|
|
171
|
+
for (const adrRef of doc.adrRefs) {
|
|
172
|
+
annotations[adrRef] = 'adr:referenced';
|
|
173
|
+
}
|
|
174
|
+
for (const workItemId of workItems) {
|
|
175
|
+
annotations[`${doc.docPath}#work-item:${workItemId}`] = 'work-item:referenced';
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return annotations;
|
|
142
180
|
}
|
|
143
181
|
|
|
144
182
|
async getElements(targetUnits?: readonly string[]): Promise<string[]> {
|
|
@@ -146,6 +184,17 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
146
184
|
return docs.flatMap((doc) => doc.concepts.map((concept) => concept.name));
|
|
147
185
|
}
|
|
148
186
|
|
|
187
|
+
async getElementRecords(targetUnits?: readonly string[]): Promise<readonly DriftElementRecord[]> {
|
|
188
|
+
const docs = await this.loadDesignDocuments(targetUnits);
|
|
189
|
+
return docs.flatMap((doc) =>
|
|
190
|
+
doc.concepts.map((concept) => ({
|
|
191
|
+
element: concept.name,
|
|
192
|
+
unitName: doc.unitName,
|
|
193
|
+
pointers: concept.pointers ?? [],
|
|
194
|
+
}))
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
149
198
|
async getElementPointers(targetUnits?: readonly string[]): Promise<Record<string, readonly string[]>> {
|
|
150
199
|
const docs = await this.loadDesignDocuments(targetUnits);
|
|
151
200
|
const map: Record<string, readonly string[]> = {};
|
|
@@ -176,6 +225,26 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
176
225
|
return map;
|
|
177
226
|
}
|
|
178
227
|
|
|
228
|
+
private async loadExplicitDocs(targetDocs: readonly string[]): Promise<readonly StructuredDesignDoc[]> {
|
|
229
|
+
const docs: StructuredDesignDoc[] = [];
|
|
230
|
+
for (const docPath of targetDocs) {
|
|
231
|
+
try {
|
|
232
|
+
const markdown = await readFile(docPath, 'utf8');
|
|
233
|
+
const unitName = inferUnitNameFromDocPath(this.docsRoot, docPath);
|
|
234
|
+
docs.push({
|
|
235
|
+
unitName,
|
|
236
|
+
docPath,
|
|
237
|
+
concepts: extractConcepts(markdown).map((concept) => ({ ...concept, type: 'class' })),
|
|
238
|
+
layerDependencies: extractLayerDependencies(markdown),
|
|
239
|
+
adrRefs: Array.from(new Set(markdown.match(ADR_PATTERN) ?? [])),
|
|
240
|
+
});
|
|
241
|
+
} catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return docs;
|
|
246
|
+
}
|
|
247
|
+
|
|
179
248
|
private async listUnitNames(): Promise<string[]> {
|
|
180
249
|
try {
|
|
181
250
|
const entries = await readdir(this.docsRoot, { withFileTypes: true });
|
|
@@ -185,3 +254,25 @@ export class MarkdownDesignDocumentAdapter implements DesignDocumentPort {
|
|
|
185
254
|
}
|
|
186
255
|
}
|
|
187
256
|
}
|
|
257
|
+
|
|
258
|
+
function extractLayerDependencies(markdown: string): Array<{ from: string; to: string }> {
|
|
259
|
+
const dependencies: Array<{ from: string; to: string }> = [];
|
|
260
|
+
const dependencyPattern = /([a-z0-9-]+)\s*(?:->|→)\s*([a-z0-9-]+)/gi;
|
|
261
|
+
for (const match of markdown.matchAll(dependencyPattern)) {
|
|
262
|
+
dependencies.push({ from: match[1], to: match[2] });
|
|
263
|
+
}
|
|
264
|
+
return dependencies;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isKnownLayer(layer: string): boolean {
|
|
268
|
+
return ['domain', 'application', 'infrastructure', 'presentation', 'test'].includes(layer);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function inferUnitNameFromDocPath(docsRoot: string, docPath: string): string {
|
|
272
|
+
const normalizedRoot = docsRoot.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
273
|
+
const normalizedPath = docPath.replace(/\\/g, '/');
|
|
274
|
+
const relativePath = normalizedPath.startsWith(`${normalizedRoot}/`)
|
|
275
|
+
? normalizedPath.slice(normalizedRoot.length + 1)
|
|
276
|
+
: normalizedPath;
|
|
277
|
+
return relativePath.split('/')[0] || 'unknown';
|
|
278
|
+
}
|