phasegate 0.152.0 → 0.152.1
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 +6 -0
- package/package.json +1 -1
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +44 -2
- package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +9 -2
- package/scripts/harness/validator-system/composition-root.ts +30 -0
- package/scripts/harness/validator-system/domain/services/l4/architecture-semantic-analysis-service.ts +124 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-architecture-semantic-source-adapter.ts +115 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.152.1] - 2026-05-12
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **G5 post-publish dogfood / WI-134 / WI-135** — wires architecture semantic policy into `L4-002` runtime validation so side-effect capability denials and decision-placement advisories are reported with file zone, evidence, confidence, and suggested owner zone.
|
|
15
|
+
|
|
10
16
|
## [0.152.0] - 2026-05-12
|
|
11
17
|
|
|
12
18
|
### Added
|
package/package.json
CHANGED
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -8,15 +8,57 @@ import type { HarnessConfigV2 } from '../../domain/harness-config.js';
|
|
|
8
8
|
export function toValidatorSystemConfig(resolvedConfig: HarnessConfigV2 | undefined): object | undefined {
|
|
9
9
|
if (!resolvedConfig) return undefined;
|
|
10
10
|
|
|
11
|
+
const l3Validators = normalizeValidators(resolvedConfig.layers.L3.validators, {
|
|
12
|
+
security: 'L3-001',
|
|
13
|
+
performance: 'L3-002',
|
|
14
|
+
coverage: 'L3-003',
|
|
15
|
+
nyquist: 'L3-004',
|
|
16
|
+
}, /^L3-\d{3}$/);
|
|
17
|
+
const l4Validators = normalizeValidators(resolvedConfig.layers.L4.validators, {
|
|
18
|
+
'drift-detect': 'L4-001',
|
|
19
|
+
'drift-detector': 'L4-001',
|
|
20
|
+
'consistency-check': 'L4-002',
|
|
21
|
+
'consistency-checker': 'L4-002',
|
|
22
|
+
'dead-code': 'L4-003',
|
|
23
|
+
'dead-code-detector': 'L4-003',
|
|
24
|
+
'doc-freshness': 'L4-004',
|
|
25
|
+
'doc-freshness-checker': 'L4-004',
|
|
26
|
+
'pointer-validation': 'L4-005',
|
|
27
|
+
'pointer-validator': 'L4-005',
|
|
28
|
+
}, /^L4-\d{3}$/);
|
|
29
|
+
|
|
11
30
|
return {
|
|
12
31
|
project: { preset: resolvedConfig.project.preset },
|
|
13
32
|
layers: {
|
|
14
33
|
L2: { enabled: resolvedConfig.layers.L2.enabled, validators: ['L2-001', 'L2-002', 'L2-003', 'L2-013', 'L2-014', 'L2-015'] },
|
|
15
|
-
L3: {
|
|
16
|
-
|
|
34
|
+
L3: {
|
|
35
|
+
enabled: resolvedConfig.layers.L3.enabled,
|
|
36
|
+
...(l3Validators.length > 0 ? { validators: l3Validators } : {}),
|
|
37
|
+
coverageThreshold: resolvedConfig.layers.L3.coverageThreshold,
|
|
38
|
+
},
|
|
39
|
+
L4: {
|
|
40
|
+
enabled: resolvedConfig.layers.L4.enabled,
|
|
41
|
+
...(l4Validators.length > 0 ? { validators: l4Validators } : {}),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
harnesses: {
|
|
45
|
+
bundleSizeLimit: resolvedConfig.harnesses.bundleSizeLimit,
|
|
46
|
+
deadCodeGC: resolvedConfig.harnesses.deadCodeGC,
|
|
17
47
|
},
|
|
48
|
+
architecture: resolvedConfig.architecture,
|
|
18
49
|
validate: {
|
|
19
50
|
failOnWarning: resolvedConfig.validate.failOnWarning,
|
|
20
51
|
},
|
|
21
52
|
};
|
|
22
53
|
}
|
|
54
|
+
|
|
55
|
+
function normalizeValidators(
|
|
56
|
+
validators: readonly string[],
|
|
57
|
+
aliases: Readonly<Record<string, string>>,
|
|
58
|
+
idPattern: RegExp,
|
|
59
|
+
): readonly string[] {
|
|
60
|
+
const normalized = validators
|
|
61
|
+
.map((validator) => aliases[validator] ?? validator)
|
|
62
|
+
.filter((validator) => idPattern.test(validator));
|
|
63
|
+
return [...new Set(normalized)];
|
|
64
|
+
}
|
|
@@ -17,6 +17,7 @@ import type { ValidatorConfigPort } from '../../domain/ports/validator-config-po
|
|
|
17
17
|
import type { DriftDetectionService } from '../../domain/services/l4/drift-detection-service.js';
|
|
18
18
|
import type { ConsistencyCheckService } from '../../domain/services/l4/consistency-check-service.js';
|
|
19
19
|
import type { DeadCodeDetectionService } from '../../domain/services/l4/dead-code-detection-service.js';
|
|
20
|
+
import type { ArchitectureSemanticAnalysisService } from '../../domain/services/l4/architecture-semantic-analysis-service.js';
|
|
20
21
|
|
|
21
22
|
interface ScheduledHarnessErrorContract {
|
|
22
23
|
readonly severity: string;
|
|
@@ -65,6 +66,7 @@ export interface RunL4ValidatorsUseCaseDeps {
|
|
|
65
66
|
driftDetectionService?: DriftDetectionService;
|
|
66
67
|
consistencyCheckService?: ConsistencyCheckService;
|
|
67
68
|
deadCodeDetectionService?: DeadCodeDetectionService;
|
|
69
|
+
architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
68
70
|
checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
69
71
|
validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
70
72
|
}
|
|
@@ -77,6 +79,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
77
79
|
private readonly driftDetectionService?: DriftDetectionService;
|
|
78
80
|
private readonly consistencyCheckService?: ConsistencyCheckService;
|
|
79
81
|
private readonly deadCodeDetectionService?: DeadCodeDetectionService;
|
|
82
|
+
private readonly architectureSemanticAnalysisService?: ArchitectureSemanticAnalysisService;
|
|
80
83
|
private readonly checkDocFreshnessUseCase?: CheckDocFreshnessUseCasePort;
|
|
81
84
|
private readonly validateDocPointersUseCase?: ValidateDocPointersUseCasePort;
|
|
82
85
|
|
|
@@ -88,6 +91,7 @@ export class RunL4ValidatorsUseCase {
|
|
|
88
91
|
this.driftDetectionService = deps.driftDetectionService;
|
|
89
92
|
this.consistencyCheckService = deps.consistencyCheckService;
|
|
90
93
|
this.deadCodeDetectionService = deps.deadCodeDetectionService;
|
|
94
|
+
this.architectureSemanticAnalysisService = deps.architectureSemanticAnalysisService;
|
|
91
95
|
this.checkDocFreshnessUseCase = deps.checkDocFreshnessUseCase;
|
|
92
96
|
this.validateDocPointersUseCase = deps.validateDocPointersUseCase;
|
|
93
97
|
}
|
|
@@ -148,10 +152,13 @@ export class RunL4ValidatorsUseCase {
|
|
|
148
152
|
const l4002Result = overrideMap.get('L4-002');
|
|
149
153
|
if (l4002Result && !l4002Result.skipped) {
|
|
150
154
|
const report = await this.consistencyCheckService.check(input.targetUnits ? [...input.targetUnits] : undefined);
|
|
151
|
-
|
|
155
|
+
const architectureSemanticErrors = this.architectureSemanticAnalysisService
|
|
156
|
+
? await this.architectureSemanticAnalysisService.analyze()
|
|
157
|
+
: [];
|
|
158
|
+
if (report.hasMismatches() || architectureSemanticErrors.length > 0) {
|
|
152
159
|
overrideMap.set(
|
|
153
160
|
'L4-002',
|
|
154
|
-
ValidationResult.fail(ValidatorId.create('L4-002'), [...report.toHarnessErrors()], 0),
|
|
161
|
+
ValidationResult.fail(ValidatorId.create('L4-002'), [...report.toHarnessErrors(), ...architectureSemanticErrors], 0),
|
|
155
162
|
);
|
|
156
163
|
}
|
|
157
164
|
}
|
|
@@ -35,9 +35,11 @@ import { MarkdownDesignDocumentAdapter } from './infrastructure/adapters/markdow
|
|
|
35
35
|
import { BiomeAstSourceCodeAnalyzerAdapter } from './infrastructure/adapters/biome-ast-source-code-analyzer-adapter.js';
|
|
36
36
|
import { AdrFoundationReferenceAdapter } from './infrastructure/adapters/adr-foundation-reference-adapter.js';
|
|
37
37
|
import { ImportGraphSourceAnalysisAdapter } from './infrastructure/adapters/import-graph-source-analysis-adapter.js';
|
|
38
|
+
import { FileSystemArchitectureSemanticSourceAdapter } from './infrastructure/adapters/file-system-architecture-semantic-source-adapter.js';
|
|
38
39
|
import { DriftDetectionService } from './domain/services/l4/drift-detection-service.js';
|
|
39
40
|
import { ConsistencyCheckService } from './domain/services/l4/consistency-check-service.js';
|
|
40
41
|
import { DeadCodeDetectionService } from './domain/services/l4/dead-code-detection-service.js';
|
|
42
|
+
import { ArchitectureSemanticAnalysisService, type ArchitectureSemanticPolicy } from './domain/services/l4/architecture-semantic-analysis-service.js';
|
|
41
43
|
import { buildPhase2Extensions } from '../phase2-extensions/composition-root.js';
|
|
42
44
|
import { RunValidatorsHandler } from './presentation/handlers/run-validators-handler.js';
|
|
43
45
|
import { RunQuickModeHandler } from './presentation/handlers/run-quick-mode-handler.js';
|
|
@@ -53,6 +55,20 @@ const DEFAULT_CONFIG = {
|
|
|
53
55
|
L4: { enabled: true, validators: ['L4-001', 'L4-002', 'L4-003', 'L4-004', 'L4-005'] },
|
|
54
56
|
},
|
|
55
57
|
validate: { failOnWarning: false },
|
|
58
|
+
architecture: {
|
|
59
|
+
capabilityPolicies: {
|
|
60
|
+
domain: { allowed: [], denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'] },
|
|
61
|
+
application: { allowed: ['time', 'random'], denied: ['filesystem', 'network', 'database', 'subprocess'] },
|
|
62
|
+
infrastructure: { allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'], denied: [] },
|
|
63
|
+
presentation: { allowed: ['user-io', 'time'], denied: ['database', 'subprocess'] },
|
|
64
|
+
},
|
|
65
|
+
decisionPolicies: {
|
|
66
|
+
domain: { expected: ['business-rule-branch', 'validation-rule', 'state-transition'], advisoryOnly: true },
|
|
67
|
+
application: { expected: ['policy-selection', 'error-construction'], advisoryOnly: true },
|
|
68
|
+
infrastructure: { expected: ['error-construction'], advisoryOnly: true },
|
|
69
|
+
presentation: { expected: ['validation-rule', 'error-construction'], advisoryOnly: true },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
56
72
|
};
|
|
57
73
|
|
|
58
74
|
/** バリデータ定義カタログ */
|
|
@@ -171,6 +187,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
171
187
|
const sourceCodeAnalyzerAdapter = new BiomeAstSourceCodeAnalyzerAdapter();
|
|
172
188
|
const adrReferencePort = new AdrFoundationReferenceAdapter();
|
|
173
189
|
const sourceAnalysisPort = new ImportGraphSourceAnalysisAdapter();
|
|
190
|
+
const architectureSemanticSourcePort = new FileSystemArchitectureSemanticSourceAdapter();
|
|
174
191
|
|
|
175
192
|
const driftDetectionService = new DriftDetectionService({
|
|
176
193
|
designDocumentPort: markdownDesignDocumentPort,
|
|
@@ -183,6 +200,10 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
183
200
|
const deadCodeDetectionService = new DeadCodeDetectionService({
|
|
184
201
|
sourceAnalysisPort,
|
|
185
202
|
});
|
|
203
|
+
const architectureSemanticAnalysisService = new ArchitectureSemanticAnalysisService({
|
|
204
|
+
sourcePort: architectureSemanticSourcePort,
|
|
205
|
+
policy: toArchitectureSemanticPolicy(configData),
|
|
206
|
+
});
|
|
186
207
|
const phase2Extensions = buildPhase2Extensions(process.cwd(), configData as never);
|
|
187
208
|
|
|
188
209
|
const runL4ValidatorsUseCase = new RunL4ValidatorsUseCase({
|
|
@@ -193,6 +214,7 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
193
214
|
driftDetectionService,
|
|
194
215
|
consistencyCheckService,
|
|
195
216
|
deadCodeDetectionService,
|
|
217
|
+
architectureSemanticAnalysisService,
|
|
196
218
|
checkDocFreshnessUseCase: phase2Extensions.checkDocFreshnessUseCase,
|
|
197
219
|
validateDocPointersUseCase: phase2Extensions.validateDocPointersUseCase,
|
|
198
220
|
});
|
|
@@ -247,3 +269,11 @@ export function createValidatorSystemModule(config?: object): ValidatorSystemMod
|
|
|
247
269
|
handlers,
|
|
248
270
|
};
|
|
249
271
|
}
|
|
272
|
+
|
|
273
|
+
function toArchitectureSemanticPolicy(configData: typeof DEFAULT_CONFIG): ArchitectureSemanticPolicy {
|
|
274
|
+
const architecture = configData.architecture;
|
|
275
|
+
return {
|
|
276
|
+
capabilityPolicies: architecture?.capabilityPolicies ?? DEFAULT_CONFIG.architecture.capabilityPolicies,
|
|
277
|
+
decisionPolicies: architecture?.decisionPolicies ?? DEFAULT_CONFIG.architecture.decisionPolicies,
|
|
278
|
+
} as ArchitectureSemanticPolicy;
|
|
279
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer domain
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-134, WI-135
|
|
5
|
+
*
|
|
6
|
+
* ArchitectureSemanticAnalysisService
|
|
7
|
+
* Architecture preset の capability / decision policy と source semantic signal を照合する advisory service.
|
|
8
|
+
*/
|
|
9
|
+
import type { HarnessErrorLike } from '../../value-objects/validation-result.js';
|
|
10
|
+
|
|
11
|
+
export type EffectCapability =
|
|
12
|
+
| 'filesystem'
|
|
13
|
+
| 'network'
|
|
14
|
+
| 'database'
|
|
15
|
+
| 'process-env'
|
|
16
|
+
| 'time'
|
|
17
|
+
| 'random'
|
|
18
|
+
| 'subprocess'
|
|
19
|
+
| 'user-io';
|
|
20
|
+
|
|
21
|
+
export type DecisionSignal =
|
|
22
|
+
| 'business-rule-branch'
|
|
23
|
+
| 'validation-rule'
|
|
24
|
+
| 'error-construction'
|
|
25
|
+
| 'state-transition'
|
|
26
|
+
| 'policy-selection';
|
|
27
|
+
|
|
28
|
+
export interface ArchitectureCapabilityPolicy {
|
|
29
|
+
readonly allowed: readonly EffectCapability[];
|
|
30
|
+
readonly denied: readonly EffectCapability[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ArchitectureDecisionPolicy {
|
|
34
|
+
readonly expected: readonly DecisionSignal[];
|
|
35
|
+
readonly advisoryOnly: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ArchitectureSemanticPolicy {
|
|
39
|
+
readonly capabilityPolicies: Readonly<Record<string, ArchitectureCapabilityPolicy>>;
|
|
40
|
+
readonly decisionPolicies: Readonly<Record<string, ArchitectureDecisionPolicy>>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SourceSemanticSignal {
|
|
44
|
+
readonly kind: EffectCapability | DecisionSignal;
|
|
45
|
+
readonly evidence: string;
|
|
46
|
+
readonly confidence: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SourceSemanticFile {
|
|
50
|
+
readonly filePath: string;
|
|
51
|
+
readonly zone: string;
|
|
52
|
+
readonly effects: readonly SourceSemanticSignal[];
|
|
53
|
+
readonly decisions: readonly SourceSemanticSignal[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ArchitectureSemanticSourcePort {
|
|
57
|
+
collectSourceSemantics(): Promise<readonly SourceSemanticFile[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ArchitectureSemanticAnalysisServiceDeps {
|
|
61
|
+
readonly sourcePort: ArchitectureSemanticSourcePort;
|
|
62
|
+
readonly policy: ArchitectureSemanticPolicy;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_DECISION_OWNER: Readonly<Record<DecisionSignal, string>> = Object.freeze({
|
|
66
|
+
'business-rule-branch': 'domain',
|
|
67
|
+
'validation-rule': 'domain',
|
|
68
|
+
'error-construction': 'application',
|
|
69
|
+
'state-transition': 'domain',
|
|
70
|
+
'policy-selection': 'application',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export class ArchitectureSemanticAnalysisService {
|
|
74
|
+
private readonly sourcePort: ArchitectureSemanticSourcePort;
|
|
75
|
+
private readonly policy: ArchitectureSemanticPolicy;
|
|
76
|
+
|
|
77
|
+
constructor(deps: ArchitectureSemanticAnalysisServiceDeps) {
|
|
78
|
+
this.sourcePort = deps.sourcePort;
|
|
79
|
+
this.policy = deps.policy;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async analyze(): Promise<readonly HarnessErrorLike[]> {
|
|
83
|
+
const files = await this.sourcePort.collectSourceSemantics();
|
|
84
|
+
return files.flatMap((file) => [
|
|
85
|
+
...this.toCapabilityFindings(file),
|
|
86
|
+
...this.toDecisionFindings(file),
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private toCapabilityFindings(file: SourceSemanticFile): HarnessErrorLike[] {
|
|
91
|
+
const zonePolicy = this.policy.capabilityPolicies[file.zone];
|
|
92
|
+
if (!zonePolicy) return [];
|
|
93
|
+
const denied = new Set(zonePolicy.denied);
|
|
94
|
+
|
|
95
|
+
return file.effects
|
|
96
|
+
.filter((effect) => denied.has(effect.kind as EffectCapability))
|
|
97
|
+
.map((effect) => this.toHarnessError(
|
|
98
|
+
`Side effect capability denied: ${effect.kind} in ${file.zone} at ${file.filePath}`,
|
|
99
|
+
`confidence=${effect.confidence}; evidence=${effect.evidence}; suggested owner zone=infrastructure/adapters`,
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private toDecisionFindings(file: SourceSemanticFile): HarnessErrorLike[] {
|
|
104
|
+
const zonePolicy = this.policy.decisionPolicies[file.zone];
|
|
105
|
+
if (!zonePolicy) return [];
|
|
106
|
+
const expected = new Set(zonePolicy.expected);
|
|
107
|
+
|
|
108
|
+
return file.decisions
|
|
109
|
+
.filter((decision) => !expected.has(decision.kind as DecisionSignal))
|
|
110
|
+
.map((decision) => this.toHarnessError(
|
|
111
|
+
`Decision placement advisory: ${decision.kind} observed in ${file.zone} at ${file.filePath}`,
|
|
112
|
+
`confidence=${decision.confidence}; evidence=${decision.evidence}; suggested owner zone=${DEFAULT_DECISION_OWNER[decision.kind as DecisionSignal] ?? 'domain'}; rollout=advisory`,
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private toHarnessError(message: string, suggestion: string): HarnessErrorLike {
|
|
117
|
+
return {
|
|
118
|
+
code: { value: 'L4-002', toString: () => 'L4-002' },
|
|
119
|
+
severity: { value: 'warning', toString: () => 'warning' },
|
|
120
|
+
message,
|
|
121
|
+
suggestion,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @layer infrastructure
|
|
3
|
+
* @unit validator-system
|
|
4
|
+
* @work-item-id WI-134, WI-135
|
|
5
|
+
*
|
|
6
|
+
* FileSystemArchitectureSemanticSourceAdapter
|
|
7
|
+
* TypeScript source から side-effect capability / decision placement signal を軽量抽出する。
|
|
8
|
+
*/
|
|
9
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import type {
|
|
12
|
+
ArchitectureSemanticSourcePort,
|
|
13
|
+
DecisionSignal,
|
|
14
|
+
EffectCapability,
|
|
15
|
+
SourceSemanticFile,
|
|
16
|
+
SourceSemanticSignal,
|
|
17
|
+
} from '../../domain/services/l4/architecture-semantic-analysis-service.js';
|
|
18
|
+
|
|
19
|
+
export class FileSystemArchitectureSemanticSourceAdapter implements ArchitectureSemanticSourcePort {
|
|
20
|
+
private readonly sourceRoot: string;
|
|
21
|
+
|
|
22
|
+
constructor(sourceRoot: string = join(process.cwd(), 'scripts/harness')) {
|
|
23
|
+
this.sourceRoot = sourceRoot;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async collectSourceSemantics(): Promise<readonly SourceSemanticFile[]> {
|
|
27
|
+
const filePaths = await walkTsFiles(this.sourceRoot);
|
|
28
|
+
const files: SourceSemanticFile[] = [];
|
|
29
|
+
|
|
30
|
+
for (const filePath of filePaths) {
|
|
31
|
+
if (isExcluded(filePath)) continue;
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFile(filePath, 'utf8');
|
|
34
|
+
files.push({
|
|
35
|
+
filePath,
|
|
36
|
+
zone: detectZone(filePath, content),
|
|
37
|
+
effects: detectEffects(content),
|
|
38
|
+
decisions: detectDecisions(content),
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function walkTsFiles(root: string): Promise<string[]> {
|
|
50
|
+
try {
|
|
51
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
52
|
+
const files = await Promise.all(entries.map(async (entry) => {
|
|
53
|
+
const fullPath = join(root, entry.name);
|
|
54
|
+
if (entry.isDirectory()) return walkTsFiles(fullPath);
|
|
55
|
+
return fullPath.endsWith('.ts') ? [fullPath] : [];
|
|
56
|
+
}));
|
|
57
|
+
return files.flat();
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isExcluded(filePath: string): boolean {
|
|
64
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
65
|
+
return /(^|\/)__tests__\//.test(normalized) || /\.test\.ts$/.test(normalized) || /(^|\/)fixtures?\//.test(normalized);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function detectZone(filePath: string, content: string): string {
|
|
69
|
+
const tagMatch = content.match(/@layer\s+([a-z][a-z0-9-]*)/);
|
|
70
|
+
if (tagMatch?.[1]) return tagMatch[1];
|
|
71
|
+
|
|
72
|
+
const normalized = filePath.replaceAll('\\', '/');
|
|
73
|
+
for (const zone of ['domain', 'application', 'infrastructure', 'presentation', 'controller', 'service', 'repository', 'core', 'ports', 'adapters']) {
|
|
74
|
+
if (normalized.includes(`/${zone}/`)) return zone;
|
|
75
|
+
}
|
|
76
|
+
return 'application';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detectEffects(content: string): SourceSemanticSignal[] {
|
|
80
|
+
const checks: Array<{ kind: EffectCapability; pattern: RegExp; evidence: string }> = [
|
|
81
|
+
{ kind: 'filesystem', pattern: /\bnode:fs\b|\bfrom\s+['"]fs['"]|\b(?:readFileSync|writeFileSync|readdirSync|statSync)\b/, evidence: 'filesystem API reference' },
|
|
82
|
+
{ kind: 'network', pattern: /\bfetch\s*\(|\bnode:https?\b|\bfrom\s+['"]https?['"]/, evidence: 'network API reference' },
|
|
83
|
+
{ kind: 'database', pattern: /\b(?:prisma|sequelize|knex|sql`|\.query\s*\()/i, evidence: 'database API reference' },
|
|
84
|
+
{ kind: 'process-env', pattern: /\bprocess\.env\b/, evidence: 'process.env reference' },
|
|
85
|
+
{ kind: 'time', pattern: /\bDate\.now\s*\(|\bnew\s+Date\s*\(/, evidence: 'time source reference' },
|
|
86
|
+
{ kind: 'random', pattern: /\bMath\.random\s*\(|\brandomUUID\s*\(/, evidence: 'random source reference' },
|
|
87
|
+
{ kind: 'subprocess', pattern: /\bnode:child_process\b|\b(?:execSync|execFileSync|spawnSync)\b/, evidence: 'subprocess API reference' },
|
|
88
|
+
{ kind: 'user-io', pattern: /\bnode:readline\b|\bprompt\s*\(/, evidence: 'user I/O reference' },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
return checks
|
|
92
|
+
.filter((check) => check.pattern.test(content))
|
|
93
|
+
.map((check) => ({ kind: check.kind, evidence: check.evidence, confidence: 0.9 }));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detectDecisions(content: string): SourceSemanticSignal[] {
|
|
97
|
+
const signals: SourceSemanticSignal[] = [];
|
|
98
|
+
const branchCount = (content.match(/\bif\s*\(|\bswitch\s*\(/g) ?? []).length;
|
|
99
|
+
if (branchCount >= 3) {
|
|
100
|
+
signals.push({ kind: 'business-rule-branch', evidence: `branch-count=${branchCount}`, confidence: 0.72 });
|
|
101
|
+
}
|
|
102
|
+
if (/\b(?:validate|isValid|required|invalid)\b/i.test(content) && /\bif\s*\(/.test(content)) {
|
|
103
|
+
signals.push({ kind: 'validation-rule', evidence: 'validation keyword with branch', confidence: 0.7 });
|
|
104
|
+
}
|
|
105
|
+
if (/\b(?:new\s+Error|HarnessError|throw\s+new)\b/.test(content)) {
|
|
106
|
+
signals.push({ kind: 'error-construction', evidence: 'error construction expression', confidence: 0.86 });
|
|
107
|
+
}
|
|
108
|
+
if (/\b(?:state|status)\s*=|transition[A-Z]\w*\s*\(/.test(content)) {
|
|
109
|
+
signals.push({ kind: 'state-transition', evidence: 'state/status transition expression', confidence: 0.78 });
|
|
110
|
+
}
|
|
111
|
+
if (/\bswitch\s*\(|\bselect[A-Z]\w*\s*\(/.test(content)) {
|
|
112
|
+
signals.push({ kind: 'policy-selection', evidence: 'policy selection branch', confidence: 0.68 });
|
|
113
|
+
}
|
|
114
|
+
return signals;
|
|
115
|
+
}
|