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.
Files changed (18) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +1 -1
  3. package/scripts/harness/biome-ast-engine/domain/value-objects/architecture-spec.ts +95 -0
  4. package/scripts/harness/config-foundation/application/mappers/validator-system-config-mapper.ts +45 -3
  5. package/scripts/harness/config-foundation/domain/services/architecture-resolution-service.ts +60 -0
  6. package/scripts/harness/config-foundation/domain/value-objects/architecture-config.ts +47 -0
  7. package/scripts/harness/config-foundation/domain/value-objects/architecture-preset-catalog.ts +75 -1
  8. package/scripts/harness/config-foundation/infrastructure/schemas/harness-config-v3.schema.json +46 -0
  9. package/scripts/harness/validator-system/application/use-cases/run-l4-validators-usecase.ts +9 -2
  10. package/scripts/harness/validator-system/composition-root.ts +30 -0
  11. package/scripts/harness/validator-system/domain/ports/source-analysis-port.ts +2 -2
  12. package/scripts/harness/validator-system/domain/services/l4/architecture-semantic-analysis-service.ts +124 -0
  13. package/scripts/harness/validator-system/domain/services/l4/dead-code-detection-service.ts +2 -2
  14. package/scripts/harness/validator-system/infrastructure/adapters/ast-performance-scanner-adapter.ts +55 -3
  15. package/scripts/harness/validator-system/infrastructure/adapters/biome-ast-source-code-analyzer-adapter.ts +5 -2
  16. package/scripts/harness/validator-system/infrastructure/adapters/file-system-architecture-semantic-source-adapter.ts +115 -0
  17. package/scripts/harness/validator-system/infrastructure/adapters/file-system-security-pattern-scanner-adapter.ts +44 -9
  18. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phasegate",
3
- "version": "0.151.1",
3
+ "version": "0.152.1",
4
4
  "packageManager": "pnpm@10.30.1",
5
5
  "description": "Phasegate — AI-agnostic quality defense toolkit. Enforces structural integrity between design intent and code.",
6
6
  "license": "MIT",
@@ -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
  });
@@ -1,22 +1,64 @@
1
1
  /**
2
2
  * @layer application
3
3
  * @unit config-foundation
4
- * @work-item-id WI-092 / WI-094 / WI-033 / WI-140
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: { enabled: resolvedConfig.layers.L3.enabled },
16
- L4: { enabled: resolvedConfig.layers.L4.enabled, validators: resolvedConfig.layers.L4.validators },
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
+ }
@@ -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
  };
@@ -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
  });
@@ -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
- if (report.hasMismatches()) {
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
  }