phasegate 0.151.1 → 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 +16 -0
- package/package.json +1 -1
- package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +95 -0
- package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +45 -3
- package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +60 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +47 -0
- package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -1
- package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +46 -0
- 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/ports/source-analysis-port.ts +2 -2
- package/scripts/harness/validator-system/domain/services/l4/architecture-semantic-analysis-service.ts +124 -0
- package/scripts/harness/validator-system/domain/services/l4/dead-code-detection-service.ts +2 -2
- package/scripts/harness/validator-system/infrastructure/adapters/ast-performance-scanner-adapter.ts +55 -3
- package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +5 -2
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-architecture-semantic-source-adapter.ts +115 -0
- package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +44 -9
- package/scripts/harness/validator-system/infrastructure/adapters/import-graph-source-analysis-adapter.ts +120 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ 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
|
+
|
|
16
|
+
## [0.152.0] - 2026-05-12
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **G5 / WI-119 / WI-120 / WI-121 / WI-134 / WI-135 — architecture semantic analysis** — strengthens code semantic analysis across L3/L4 validators and architecture presets.
|
|
21
|
+
- L4-003 dead-code detection now builds a real import/export/re-export/dynamic-import graph and reports unused export candidates with reviewable reasons while preserving public/test/generated boundaries.
|
|
22
|
+
- L3-001 security scanning detects representative OpenAI, GitHub, AWS, npm, Slack, and keyword-context token families, supports explicit fixture allowlisting, and redacts secret values from findings.
|
|
23
|
+
- L3-002 performance scanning defines practical static scope with file-size thresholds, await-in-loop, synchronous I/O, large literal checks, and inline suppression for accepted migration/batch cases.
|
|
24
|
+
- Architecture presets now expose side-effect capability policies and advisory decision-placement responsibilities separately from dependency-direction checks.
|
|
25
|
+
|
|
10
26
|
## [0.151.1] - 2026-05-12
|
|
11
27
|
|
|
12
28
|
### Fixed
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ export type ArchitectureSpec = {
|
|
|
7
7
|
readonly layers: readonly string[];
|
|
8
8
|
readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
|
|
9
9
|
readonly metadataTags: ArchitectureMetadataTags;
|
|
10
|
+
readonly capabilityPolicies: Readonly<Record<string, ArchitectureCapabilityPolicy>>;
|
|
11
|
+
readonly decisionPolicies: Readonly<Record<string, ArchitectureDecisionPolicy>>;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export type ArchitectureMetadataTags = {
|
|
@@ -14,10 +16,39 @@ export type ArchitectureMetadataTags = {
|
|
|
14
16
|
readonly layer: string;
|
|
15
17
|
};
|
|
16
18
|
|
|
19
|
+
export type EffectCapability =
|
|
20
|
+
| 'filesystem'
|
|
21
|
+
| 'network'
|
|
22
|
+
| 'database'
|
|
23
|
+
| 'process-env'
|
|
24
|
+
| 'time'
|
|
25
|
+
| 'random'
|
|
26
|
+
| 'subprocess'
|
|
27
|
+
| 'user-io';
|
|
28
|
+
|
|
29
|
+
export type DecisionSignal =
|
|
30
|
+
| 'business-rule-branch'
|
|
31
|
+
| 'validation-rule'
|
|
32
|
+
| 'error-construction'
|
|
33
|
+
| 'state-transition'
|
|
34
|
+
| 'policy-selection';
|
|
35
|
+
|
|
36
|
+
export type ArchitectureCapabilityPolicy = {
|
|
37
|
+
readonly allowed: readonly EffectCapability[];
|
|
38
|
+
readonly denied: readonly EffectCapability[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ArchitectureDecisionPolicy = {
|
|
42
|
+
readonly expected: readonly DecisionSignal[];
|
|
43
|
+
readonly advisoryOnly: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
17
46
|
export type ArchitectureSpecInput = {
|
|
18
47
|
readonly layers: readonly string[];
|
|
19
48
|
readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
|
|
20
49
|
readonly metadataTags?: Partial<ArchitectureMetadataTags>;
|
|
50
|
+
readonly capabilityPolicies?: Readonly<Record<string, ArchitectureCapabilityPolicy>>;
|
|
51
|
+
readonly decisionPolicies?: Readonly<Record<string, ArchitectureDecisionPolicy>>;
|
|
21
52
|
};
|
|
22
53
|
|
|
23
54
|
export const DEFAULT_METADATA_TAGS: ArchitectureMetadataTags = Object.freeze({
|
|
@@ -37,6 +68,32 @@ const freezeDependencyMap = (
|
|
|
37
68
|
return Object.freeze(frozen);
|
|
38
69
|
};
|
|
39
70
|
|
|
71
|
+
const freezeCapabilityPolicies = (
|
|
72
|
+
policies: Readonly<Record<string, ArchitectureCapabilityPolicy>> = {},
|
|
73
|
+
): Readonly<Record<string, ArchitectureCapabilityPolicy>> => {
|
|
74
|
+
const frozen: Record<string, ArchitectureCapabilityPolicy> = {};
|
|
75
|
+
for (const [zone, policy] of Object.entries(policies)) {
|
|
76
|
+
frozen[zone] = Object.freeze({
|
|
77
|
+
allowed: Object.freeze([...policy.allowed]),
|
|
78
|
+
denied: Object.freeze([...policy.denied]),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze(frozen);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const freezeDecisionPolicies = (
|
|
85
|
+
policies: Readonly<Record<string, ArchitectureDecisionPolicy>> = {},
|
|
86
|
+
): Readonly<Record<string, ArchitectureDecisionPolicy>> => {
|
|
87
|
+
const frozen: Record<string, ArchitectureDecisionPolicy> = {};
|
|
88
|
+
for (const [zone, policy] of Object.entries(policies)) {
|
|
89
|
+
frozen[zone] = Object.freeze({
|
|
90
|
+
expected: Object.freeze([...policy.expected]),
|
|
91
|
+
advisoryOnly: policy.advisoryOnly,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return Object.freeze(frozen);
|
|
95
|
+
};
|
|
96
|
+
|
|
40
97
|
export const freezeArchitectureSpec = (spec: ArchitectureSpecInput): ArchitectureSpec => {
|
|
41
98
|
return Object.freeze({
|
|
42
99
|
layers: Object.freeze([...spec.layers]),
|
|
@@ -45,6 +102,8 @@ export const freezeArchitectureSpec = (spec: ArchitectureSpecInput): Architectur
|
|
|
45
102
|
...DEFAULT_METADATA_TAGS,
|
|
46
103
|
...spec.metadataTags,
|
|
47
104
|
}),
|
|
105
|
+
capabilityPolicies: freezeCapabilityPolicies(spec.capabilityPolicies),
|
|
106
|
+
decisionPolicies: freezeDecisionPolicies(spec.decisionPolicies),
|
|
48
107
|
});
|
|
49
108
|
};
|
|
50
109
|
|
|
@@ -56,4 +115,40 @@ export const CLEAN_PRESET_SPEC: ArchitectureSpec = freezeArchitectureSpec({
|
|
|
56
115
|
infrastructure: ['infrastructure', 'application', 'domain'],
|
|
57
116
|
presentation: ['presentation', 'application', 'domain'],
|
|
58
117
|
},
|
|
118
|
+
capabilityPolicies: {
|
|
119
|
+
domain: {
|
|
120
|
+
allowed: [],
|
|
121
|
+
denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'],
|
|
122
|
+
},
|
|
123
|
+
application: {
|
|
124
|
+
allowed: ['time', 'random'],
|
|
125
|
+
denied: ['filesystem', 'network', 'database', 'subprocess'],
|
|
126
|
+
},
|
|
127
|
+
infrastructure: {
|
|
128
|
+
allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'],
|
|
129
|
+
denied: [],
|
|
130
|
+
},
|
|
131
|
+
presentation: {
|
|
132
|
+
allowed: ['user-io', 'time'],
|
|
133
|
+
denied: ['database', 'subprocess'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
decisionPolicies: {
|
|
137
|
+
domain: {
|
|
138
|
+
expected: ['business-rule-branch', 'validation-rule', 'state-transition'],
|
|
139
|
+
advisoryOnly: true,
|
|
140
|
+
},
|
|
141
|
+
application: {
|
|
142
|
+
expected: ['policy-selection', 'error-construction'],
|
|
143
|
+
advisoryOnly: true,
|
|
144
|
+
},
|
|
145
|
+
infrastructure: {
|
|
146
|
+
expected: ['error-construction'],
|
|
147
|
+
advisoryOnly: true,
|
|
148
|
+
},
|
|
149
|
+
presentation: {
|
|
150
|
+
expected: ['validation-rule', 'error-construction'],
|
|
151
|
+
advisoryOnly: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
59
154
|
});
|
package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts
CHANGED
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @layer application
|
|
3
3
|
* @unit config-foundation
|
|
4
|
-
* @work-item-id WI-
|
|
4
|
+
* @work-item-id WI-133
|
|
5
5
|
*/
|
|
6
6
|
import type { HarnessConfigV2 } from '../../domain/harness-config.js';
|
|
7
7
|
|
|
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
|
+
}
|
package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
freezeArchitectureDocument,
|
|
9
9
|
type ArchitectureConfigDocument,
|
|
10
10
|
type ArchitectureConfigSource,
|
|
11
|
+
type CapabilityPolicy,
|
|
12
|
+
type DecisionPolicy,
|
|
11
13
|
type ArchitectureLayerDetection,
|
|
12
14
|
type ArchitectureMetadataTags,
|
|
13
15
|
type ArchitecturePresetId,
|
|
@@ -48,6 +50,32 @@ const cloneDependencies = (
|
|
|
48
50
|
return cloned;
|
|
49
51
|
};
|
|
50
52
|
|
|
53
|
+
const cloneCapabilityPolicies = (
|
|
54
|
+
source: Readonly<Record<string, CapabilityPolicy>>
|
|
55
|
+
): Record<string, CapabilityPolicy> => {
|
|
56
|
+
const cloned: Record<string, CapabilityPolicy> = {};
|
|
57
|
+
for (const [key, value] of Object.entries(source)) {
|
|
58
|
+
cloned[key] = {
|
|
59
|
+
allowed: [...value.allowed],
|
|
60
|
+
denied: [...value.denied],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return cloned;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const cloneDecisionPolicies = (
|
|
67
|
+
source: Readonly<Record<string, DecisionPolicy>>
|
|
68
|
+
): Record<string, DecisionPolicy> => {
|
|
69
|
+
const cloned: Record<string, DecisionPolicy> = {};
|
|
70
|
+
for (const [key, value] of Object.entries(source)) {
|
|
71
|
+
cloned[key] = {
|
|
72
|
+
expected: [...value.expected],
|
|
73
|
+
advisoryOnly: value.advisoryOnly,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return cloned;
|
|
77
|
+
};
|
|
78
|
+
|
|
51
79
|
const lookupPresetDefinition = (
|
|
52
80
|
preset: ArchitecturePresetId
|
|
53
81
|
): ArchitecturePresetDefinition | null => {
|
|
@@ -92,6 +120,28 @@ const mergeDependencies = (
|
|
|
92
120
|
return preset !== null ? cloneDependencies(preset.allowedDependencies) : {};
|
|
93
121
|
};
|
|
94
122
|
|
|
123
|
+
const mergeCapabilityPolicies = (
|
|
124
|
+
preset: ArchitecturePresetDefinition | null,
|
|
125
|
+
override: Readonly<Record<string, CapabilityPolicy>> | undefined
|
|
126
|
+
): Record<string, CapabilityPolicy> => {
|
|
127
|
+
if (override !== undefined) {
|
|
128
|
+
return cloneCapabilityPolicies(override);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return preset !== null ? cloneCapabilityPolicies(preset.capabilityPolicies) : {};
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const mergeDecisionPolicies = (
|
|
135
|
+
preset: ArchitecturePresetDefinition | null,
|
|
136
|
+
override: Readonly<Record<string, DecisionPolicy>> | undefined
|
|
137
|
+
): Record<string, DecisionPolicy> => {
|
|
138
|
+
if (override !== undefined) {
|
|
139
|
+
return cloneDecisionPolicies(override);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return preset !== null ? cloneDecisionPolicies(preset.decisionPolicies) : {};
|
|
143
|
+
};
|
|
144
|
+
|
|
95
145
|
const mergeMetadataTags = (
|
|
96
146
|
override: Partial<ArchitectureMetadataTags> | undefined
|
|
97
147
|
): ArchitectureMetadataTags => {
|
|
@@ -230,6 +280,14 @@ export class ArchitectureResolutionService {
|
|
|
230
280
|
presetDefinition,
|
|
231
281
|
effectiveSource.allowedDependencies
|
|
232
282
|
);
|
|
283
|
+
const capabilityPolicies = mergeCapabilityPolicies(
|
|
284
|
+
presetDefinition,
|
|
285
|
+
effectiveSource.capabilityPolicies,
|
|
286
|
+
);
|
|
287
|
+
const decisionPolicies = mergeDecisionPolicies(
|
|
288
|
+
presetDefinition,
|
|
289
|
+
effectiveSource.decisionPolicies,
|
|
290
|
+
);
|
|
233
291
|
|
|
234
292
|
validateDependencyKeys(layers, allowedDependencies);
|
|
235
293
|
validateDependencyValues(layers, allowedDependencies);
|
|
@@ -247,6 +305,8 @@ export class ArchitectureResolutionService {
|
|
|
247
305
|
allowedDependencies: freezeDependencies(allowedDependencies),
|
|
248
306
|
metadataTags: mergeMetadataTags(effectiveSource.metadataTags),
|
|
249
307
|
layerDetection,
|
|
308
|
+
capabilityPolicies,
|
|
309
|
+
decisionPolicies,
|
|
250
310
|
});
|
|
251
311
|
|
|
252
312
|
return Object.freeze({
|
|
@@ -25,12 +25,41 @@ export interface ArchitectureLayerDetection {
|
|
|
25
25
|
readonly byTag: boolean;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
export type EffectCapability =
|
|
29
|
+
| 'filesystem'
|
|
30
|
+
| 'network'
|
|
31
|
+
| 'database'
|
|
32
|
+
| 'process-env'
|
|
33
|
+
| 'time'
|
|
34
|
+
| 'random'
|
|
35
|
+
| 'subprocess'
|
|
36
|
+
| 'user-io';
|
|
37
|
+
|
|
38
|
+
export type DecisionSignal =
|
|
39
|
+
| 'business-rule-branch'
|
|
40
|
+
| 'validation-rule'
|
|
41
|
+
| 'error-construction'
|
|
42
|
+
| 'state-transition'
|
|
43
|
+
| 'policy-selection';
|
|
44
|
+
|
|
45
|
+
export interface CapabilityPolicy {
|
|
46
|
+
readonly allowed: readonly EffectCapability[];
|
|
47
|
+
readonly denied: readonly EffectCapability[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DecisionPolicy {
|
|
51
|
+
readonly expected: readonly DecisionSignal[];
|
|
52
|
+
readonly advisoryOnly: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
28
55
|
export interface ArchitectureConfigSource {
|
|
29
56
|
readonly preset: ArchitecturePresetId;
|
|
30
57
|
readonly layers?: readonly string[];
|
|
31
58
|
readonly allowedDependencies?: Readonly<Record<string, readonly string[]>>;
|
|
32
59
|
readonly metadataTags?: Partial<ArchitectureMetadataTags>;
|
|
33
60
|
readonly layerDetection?: Partial<ArchitectureLayerDetection>;
|
|
61
|
+
readonly capabilityPolicies?: Readonly<Record<string, CapabilityPolicy>>;
|
|
62
|
+
readonly decisionPolicies?: Readonly<Record<string, DecisionPolicy>>;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
export interface ArchitectureConfigDocument {
|
|
@@ -39,6 +68,8 @@ export interface ArchitectureConfigDocument {
|
|
|
39
68
|
readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
|
|
40
69
|
readonly metadataTags: ArchitectureMetadataTags;
|
|
41
70
|
readonly layerDetection: ArchitectureLayerDetection;
|
|
71
|
+
readonly capabilityPolicies: Readonly<Record<string, CapabilityPolicy>>;
|
|
72
|
+
readonly decisionPolicies: Readonly<Record<string, DecisionPolicy>>;
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
export const isArchitecturePresetId = (value: unknown): value is ArchitecturePresetId => {
|
|
@@ -51,10 +82,24 @@ export const freezeArchitectureDocument = (
|
|
|
51
82
|
document: ArchitectureConfigDocument
|
|
52
83
|
): ArchitectureConfigDocument => {
|
|
53
84
|
const frozenDependencies: Record<string, readonly string[]> = {};
|
|
85
|
+
const frozenCapabilityPolicies: Record<string, CapabilityPolicy> = {};
|
|
86
|
+
const frozenDecisionPolicies: Record<string, DecisionPolicy> = {};
|
|
54
87
|
|
|
55
88
|
for (const [key, value] of Object.entries(document.allowedDependencies)) {
|
|
56
89
|
frozenDependencies[key] = Object.freeze([...value]);
|
|
57
90
|
}
|
|
91
|
+
for (const [key, value] of Object.entries(document.capabilityPolicies)) {
|
|
92
|
+
frozenCapabilityPolicies[key] = Object.freeze({
|
|
93
|
+
allowed: Object.freeze([...value.allowed]),
|
|
94
|
+
denied: Object.freeze([...value.denied]),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
for (const [key, value] of Object.entries(document.decisionPolicies)) {
|
|
98
|
+
frozenDecisionPolicies[key] = Object.freeze({
|
|
99
|
+
expected: Object.freeze([...value.expected]),
|
|
100
|
+
advisoryOnly: value.advisoryOnly,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
58
103
|
|
|
59
104
|
return Object.freeze({
|
|
60
105
|
preset: document.preset,
|
|
@@ -62,5 +107,7 @@ export const freezeArchitectureDocument = (
|
|
|
62
107
|
allowedDependencies: Object.freeze(frozenDependencies),
|
|
63
108
|
metadataTags: Object.freeze({ ...document.metadataTags }),
|
|
64
109
|
layerDetection: Object.freeze({ ...document.layerDetection }),
|
|
110
|
+
capabilityPolicies: Object.freeze(frozenCapabilityPolicies),
|
|
111
|
+
decisionPolicies: Object.freeze(frozenDecisionPolicies),
|
|
65
112
|
});
|
|
66
113
|
};
|
package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts
CHANGED
|
@@ -3,23 +3,41 @@
|
|
|
3
3
|
* @unit config-foundation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ArchitecturePresetId } from './architecture-config.js';
|
|
6
|
+
import type { ArchitecturePresetId, CapabilityPolicy, DecisionPolicy } from './architecture-config.js';
|
|
7
7
|
|
|
8
8
|
export interface ArchitecturePresetDefinition {
|
|
9
9
|
readonly layers: readonly string[];
|
|
10
10
|
readonly allowedDependencies: Readonly<Record<string, readonly string[]>>;
|
|
11
|
+
readonly capabilityPolicies: Readonly<Record<string, CapabilityPolicy>>;
|
|
12
|
+
readonly decisionPolicies: Readonly<Record<string, DecisionPolicy>>;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const freeze = (definition: ArchitecturePresetDefinition): ArchitecturePresetDefinition => {
|
|
14
16
|
const dependencies: Record<string, readonly string[]> = {};
|
|
17
|
+
const capabilityPolicies: Record<string, CapabilityPolicy> = {};
|
|
18
|
+
const decisionPolicies: Record<string, DecisionPolicy> = {};
|
|
15
19
|
|
|
16
20
|
for (const [key, value] of Object.entries(definition.allowedDependencies)) {
|
|
17
21
|
dependencies[key] = Object.freeze([...value]);
|
|
18
22
|
}
|
|
23
|
+
for (const [key, value] of Object.entries(definition.capabilityPolicies)) {
|
|
24
|
+
capabilityPolicies[key] = Object.freeze({
|
|
25
|
+
allowed: Object.freeze([...value.allowed]),
|
|
26
|
+
denied: Object.freeze([...value.denied]),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
for (const [key, value] of Object.entries(definition.decisionPolicies)) {
|
|
30
|
+
decisionPolicies[key] = Object.freeze({
|
|
31
|
+
expected: Object.freeze([...value.expected]),
|
|
32
|
+
advisoryOnly: value.advisoryOnly,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
19
35
|
|
|
20
36
|
return Object.freeze({
|
|
21
37
|
layers: Object.freeze([...definition.layers]),
|
|
22
38
|
allowedDependencies: Object.freeze(dependencies),
|
|
39
|
+
capabilityPolicies: Object.freeze(capabilityPolicies),
|
|
40
|
+
decisionPolicies: Object.freeze(decisionPolicies),
|
|
23
41
|
});
|
|
24
42
|
};
|
|
25
43
|
|
|
@@ -34,6 +52,18 @@ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
|
|
|
34
52
|
infrastructure: ['infrastructure', 'application', 'domain'],
|
|
35
53
|
presentation: ['presentation', 'application', 'domain'],
|
|
36
54
|
},
|
|
55
|
+
capabilityPolicies: {
|
|
56
|
+
domain: { allowed: [], denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'] },
|
|
57
|
+
application: { allowed: ['time', 'random'], denied: ['filesystem', 'network', 'database', 'subprocess'] },
|
|
58
|
+
infrastructure: { allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'], denied: [] },
|
|
59
|
+
presentation: { allowed: ['user-io', 'time'], denied: ['database', 'subprocess'] },
|
|
60
|
+
},
|
|
61
|
+
decisionPolicies: {
|
|
62
|
+
domain: { expected: ['business-rule-branch', 'validation-rule', 'state-transition'], advisoryOnly: true },
|
|
63
|
+
application: { expected: ['policy-selection', 'error-construction'], advisoryOnly: true },
|
|
64
|
+
infrastructure: { expected: ['error-construction'], advisoryOnly: true },
|
|
65
|
+
presentation: { expected: ['validation-rule', 'error-construction'], advisoryOnly: true },
|
|
66
|
+
},
|
|
37
67
|
}),
|
|
38
68
|
'strict-ddd': freeze({
|
|
39
69
|
layers: ['domain', 'application', 'infrastructure', 'presentation'],
|
|
@@ -43,6 +73,18 @@ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
|
|
|
43
73
|
infrastructure: ['infrastructure', 'application', 'domain'],
|
|
44
74
|
presentation: ['presentation', 'application'],
|
|
45
75
|
},
|
|
76
|
+
capabilityPolicies: {
|
|
77
|
+
domain: { allowed: [], denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'] },
|
|
78
|
+
application: { allowed: ['time'], denied: ['filesystem', 'network', 'database', 'random', 'subprocess'] },
|
|
79
|
+
infrastructure: { allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'], denied: [] },
|
|
80
|
+
presentation: { allowed: ['user-io'], denied: ['database', 'subprocess'] },
|
|
81
|
+
},
|
|
82
|
+
decisionPolicies: {
|
|
83
|
+
domain: { expected: ['business-rule-branch', 'validation-rule', 'state-transition'], advisoryOnly: true },
|
|
84
|
+
application: { expected: ['policy-selection'], advisoryOnly: true },
|
|
85
|
+
infrastructure: { expected: [], advisoryOnly: true },
|
|
86
|
+
presentation: { expected: ['error-construction'], advisoryOnly: true },
|
|
87
|
+
},
|
|
46
88
|
}),
|
|
47
89
|
onion: freeze({
|
|
48
90
|
layers: ['domain', 'application', 'interface'],
|
|
@@ -51,6 +93,16 @@ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
|
|
|
51
93
|
application: ['application', 'domain'],
|
|
52
94
|
interface: ['interface', 'application', 'domain'],
|
|
53
95
|
},
|
|
96
|
+
capabilityPolicies: {
|
|
97
|
+
domain: { allowed: [], denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'] },
|
|
98
|
+
application: { allowed: ['time', 'random'], denied: ['filesystem', 'network', 'database', 'subprocess'] },
|
|
99
|
+
interface: { allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'], denied: [] },
|
|
100
|
+
},
|
|
101
|
+
decisionPolicies: {
|
|
102
|
+
domain: { expected: ['business-rule-branch', 'validation-rule', 'state-transition'], advisoryOnly: true },
|
|
103
|
+
application: { expected: ['policy-selection', 'error-construction'], advisoryOnly: true },
|
|
104
|
+
interface: { expected: ['error-construction'], advisoryOnly: true },
|
|
105
|
+
},
|
|
54
106
|
}),
|
|
55
107
|
hexagonal: freeze({
|
|
56
108
|
layers: ['core', 'ports', 'adapters'],
|
|
@@ -59,6 +111,16 @@ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
|
|
|
59
111
|
ports: ['ports', 'core'],
|
|
60
112
|
adapters: ['adapters', 'ports', 'core'],
|
|
61
113
|
},
|
|
114
|
+
capabilityPolicies: {
|
|
115
|
+
core: { allowed: [], denied: ['filesystem', 'network', 'database', 'process-env', 'subprocess', 'user-io'] },
|
|
116
|
+
ports: { allowed: [], denied: ['filesystem', 'network', 'database', 'subprocess'] },
|
|
117
|
+
adapters: { allowed: ['filesystem', 'network', 'database', 'process-env', 'time', 'random', 'subprocess', 'user-io'], denied: [] },
|
|
118
|
+
},
|
|
119
|
+
decisionPolicies: {
|
|
120
|
+
core: { expected: ['business-rule-branch', 'validation-rule', 'state-transition'], advisoryOnly: true },
|
|
121
|
+
ports: { expected: [], advisoryOnly: true },
|
|
122
|
+
adapters: { expected: ['error-construction'], advisoryOnly: true },
|
|
123
|
+
},
|
|
62
124
|
}),
|
|
63
125
|
layered: freeze({
|
|
64
126
|
layers: ['controller', 'service', 'repository'],
|
|
@@ -67,9 +129,21 @@ export const ARCHITECTURE_PRESET_CATALOG: Readonly<
|
|
|
67
129
|
service: ['service', 'repository'],
|
|
68
130
|
repository: ['repository'],
|
|
69
131
|
},
|
|
132
|
+
capabilityPolicies: {
|
|
133
|
+
controller: { allowed: ['user-io'], denied: ['database', 'filesystem', 'subprocess'] },
|
|
134
|
+
service: { allowed: ['time', 'random'], denied: ['filesystem', 'network', 'database', 'subprocess'] },
|
|
135
|
+
repository: { allowed: ['database', 'filesystem', 'network', 'process-env'], denied: ['user-io'] },
|
|
136
|
+
},
|
|
137
|
+
decisionPolicies: {
|
|
138
|
+
controller: { expected: ['validation-rule', 'error-construction'], advisoryOnly: true },
|
|
139
|
+
service: { expected: ['business-rule-branch', 'policy-selection', 'state-transition'], advisoryOnly: true },
|
|
140
|
+
repository: { expected: ['error-construction'], advisoryOnly: true },
|
|
141
|
+
},
|
|
70
142
|
}),
|
|
71
143
|
flat: freeze({
|
|
72
144
|
layers: [],
|
|
73
145
|
allowedDependencies: {},
|
|
146
|
+
capabilityPolicies: {},
|
|
147
|
+
decisionPolicies: {},
|
|
74
148
|
}),
|
|
75
149
|
});
|
package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json
CHANGED
|
@@ -12,6 +12,16 @@
|
|
|
12
12
|
"paths",
|
|
13
13
|
"reporting"
|
|
14
14
|
],
|
|
15
|
+
"$defs": {
|
|
16
|
+
"effectCapability": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"enum": ["filesystem", "network", "database", "process-env", "time", "random", "subprocess", "user-io"]
|
|
19
|
+
},
|
|
20
|
+
"decisionSignal": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": ["business-rule-branch", "validation-rule", "error-construction", "state-transition", "policy-selection"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
15
25
|
"properties": {
|
|
16
26
|
"project": {
|
|
17
27
|
"type": "object",
|
|
@@ -565,6 +575,42 @@
|
|
|
565
575
|
"byPath": { "type": "boolean" },
|
|
566
576
|
"byTag": { "type": "boolean" }
|
|
567
577
|
}
|
|
578
|
+
},
|
|
579
|
+
"capabilityPolicies": {
|
|
580
|
+
"type": "object",
|
|
581
|
+
"additionalProperties": {
|
|
582
|
+
"type": "object",
|
|
583
|
+
"additionalProperties": false,
|
|
584
|
+
"required": ["allowed", "denied"],
|
|
585
|
+
"properties": {
|
|
586
|
+
"allowed": {
|
|
587
|
+
"type": "array",
|
|
588
|
+
"items": { "$ref": "#/$defs/effectCapability" },
|
|
589
|
+
"uniqueItems": true
|
|
590
|
+
},
|
|
591
|
+
"denied": {
|
|
592
|
+
"type": "array",
|
|
593
|
+
"items": { "$ref": "#/$defs/effectCapability" },
|
|
594
|
+
"uniqueItems": true
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
"decisionPolicies": {
|
|
600
|
+
"type": "object",
|
|
601
|
+
"additionalProperties": {
|
|
602
|
+
"type": "object",
|
|
603
|
+
"additionalProperties": false,
|
|
604
|
+
"required": ["expected", "advisoryOnly"],
|
|
605
|
+
"properties": {
|
|
606
|
+
"expected": {
|
|
607
|
+
"type": "array",
|
|
608
|
+
"items": { "$ref": "#/$defs/decisionSignal" },
|
|
609
|
+
"uniqueItems": true
|
|
610
|
+
},
|
|
611
|
+
"advisoryOnly": { "type": "boolean" }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
568
614
|
}
|
|
569
615
|
},
|
|
570
616
|
"allOf": [
|
|
@@ -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
|
}
|