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
@@ -6,39 +6,82 @@
6
6
  */
7
7
  import type { SourceAnalysisPort, ImportGraphData } from '../../domain/ports/source-analysis-port.js';
8
8
  import { readdir, readFile } from 'node:fs/promises';
9
- import { join } from 'node:path';
9
+ import { dirname, extname, join, normalize, resolve } from 'node:path';
10
10
 
11
11
  const HARNESS_ROOT = join(process.cwd(), 'scripts', 'harness');
12
12
  const IMPORT_PATTERN = /import\s+(?:type\s+)?(.+?)\s+from\s+['"]([^'"]+)['"]/g;
13
- const EXPORT_PATTERN = /export\s+(?:class|interface|type|function|const)\s+(\w+)/g;
13
+ const SIDE_EFFECT_IMPORT_PATTERN = /import\s+['"]([^'"]+)['"]/g;
14
+ const DYNAMIC_IMPORT_PATTERN = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
15
+ const RE_EXPORT_NAMED_PATTERN = /export\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
16
+ const RE_EXPORT_ALL_PATTERN = /export\s+\*\s+from\s+['"]([^'"]+)['"]/g;
17
+ const EXPORT_PATTERN = /export\s+(?:declare\s+)?(?:abstract\s+)?(?:class|interface|type|function|const|let|var|enum)\s+(\w+)/g;
18
+ const EXPORT_LIST_PATTERN = /export\s+(?:type\s+)?\{([^}]+)\}/g;
14
19
 
15
20
  export class ImportGraphSourceAnalysisAdapter implements SourceAnalysisPort {
21
+ private readonly root: string;
22
+
23
+ constructor(root: string = HARNESS_ROOT) {
24
+ this.root = root;
25
+ }
26
+
16
27
  async getImportGraph(): Promise<ImportGraphData> {
17
- const filePaths = await walkTsFiles(HARNESS_ROOT);
18
- const nodes: Array<{ filePath: string; exports: readonly string[] }> = [];
19
- const edges: Array<{ from: string; to: string; importedNames: readonly string[] }> = [];
28
+ const filePaths = await walkTsFiles(this.root);
29
+ const fileSet = new Set(filePaths.map((filePath) => normalize(filePath)));
30
+ const nodes: Array<{ filePath: string; exports: readonly string[]; excludedReason?: string }> = [];
31
+ const edges: Array<{ from: string; to: string; importedNames: readonly string[]; kind: string }> = [];
20
32
 
21
33
  for (const filePath of filePaths) {
22
34
  try {
23
35
  const content = await readFile(filePath, 'utf8');
36
+ const exports = extractExports(content);
37
+ const excludedReason = classifyDeadCodeExclusion(filePath);
24
38
  nodes.push({
25
39
  filePath,
26
- exports: Array.from(content.matchAll(EXPORT_PATTERN), (match) => match[1]),
40
+ exports,
41
+ ...(excludedReason ? { excludedReason } : {}),
27
42
  });
28
43
 
29
44
  for (const match of content.matchAll(IMPORT_PATTERN)) {
45
+ const target = resolveImportTarget(filePath, match[2] ?? '', fileSet);
46
+ if (!target) continue;
47
+ edges.push({
48
+ from: filePath,
49
+ to: target,
50
+ importedNames: normalizeImportNames(match[1] ?? ''),
51
+ kind: 'static-import',
52
+ });
53
+ }
54
+ for (const match of content.matchAll(SIDE_EFFECT_IMPORT_PATTERN)) {
55
+ const target = resolveImportTarget(filePath, match[1] ?? '', fileSet);
56
+ if (!target) continue;
57
+ edges.push({ from: filePath, to: target, importedNames: ['*'], kind: 'side-effect-import' });
58
+ }
59
+ for (const match of content.matchAll(DYNAMIC_IMPORT_PATTERN)) {
60
+ const target = resolveImportTarget(filePath, match[1] ?? '', fileSet);
61
+ if (!target) continue;
62
+ edges.push({ from: filePath, to: target, importedNames: ['*'], kind: 'dynamic-import' });
63
+ }
64
+ for (const match of content.matchAll(RE_EXPORT_NAMED_PATTERN)) {
65
+ const target = resolveImportTarget(filePath, match[2] ?? '', fileSet);
66
+ if (!target) continue;
30
67
  edges.push({
31
68
  from: filePath,
32
- to: match[2],
69
+ to: target,
33
70
  importedNames: normalizeImportNames(match[1] ?? ''),
71
+ kind: 're-export',
34
72
  });
35
73
  }
74
+ for (const match of content.matchAll(RE_EXPORT_ALL_PATTERN)) {
75
+ const target = resolveImportTarget(filePath, match[1] ?? '', fileSet);
76
+ if (!target) continue;
77
+ edges.push({ from: filePath, to: target, importedNames: ['*'], kind: 're-export-all' });
78
+ }
36
79
  } catch {
37
80
  continue;
38
81
  }
39
82
  }
40
83
 
41
- return { nodes, edges, unusedExports: [], unreachableCode: [] };
84
+ return { nodes, edges, unusedExports: detectUnusedExports(nodes, edges), unreachableCode: [] };
42
85
  }
43
86
  }
44
87
 
@@ -62,6 +105,7 @@ function normalizeImportNames(clause: string): string[] {
62
105
  const cleaned = clause
63
106
  .replace(/\btype\s+/g, '')
64
107
  .replace(/\s+as\s+\w+/g, '')
108
+ .replace(/\*\s+as\s+(\w+)/g, '*')
65
109
  .replace(/[{}]/g, ',');
66
110
 
67
111
  return cleaned
@@ -69,3 +113,71 @@ function normalizeImportNames(clause: string): string[] {
69
113
  .map((part) => part.trim())
70
114
  .filter((part) => part.length > 0);
71
115
  }
116
+
117
+ function extractExports(content: string): string[] {
118
+ const names = new Set<string>();
119
+ for (const match of content.matchAll(EXPORT_PATTERN)) {
120
+ if (match[1]) names.add(match[1]);
121
+ }
122
+ for (const match of content.matchAll(EXPORT_LIST_PATTERN)) {
123
+ for (const name of normalizeImportNames(match[1] ?? '')) {
124
+ const [exportName] = name.split(/\s+as\s+/).map((part) => part.trim()).reverse();
125
+ if (exportName) names.add(exportName);
126
+ }
127
+ }
128
+ if (/export\s+default\b/.test(content)) {
129
+ names.add('default');
130
+ }
131
+ return [...names];
132
+ }
133
+
134
+ function resolveImportTarget(fromFile: string, specifier: string, fileSet: ReadonlySet<string>): string | null {
135
+ if (!specifier.startsWith('.')) return null;
136
+ const base = resolve(dirname(fromFile), specifier);
137
+ const candidates = extname(base)
138
+ ? [base]
139
+ : [`${base}.ts`, `${base}.tsx`, join(base, 'index.ts'), join(base, 'index.tsx')];
140
+ for (const candidate of candidates) {
141
+ const normalized = normalize(candidate);
142
+ if (fileSet.has(normalized)) return normalized;
143
+ }
144
+ return null;
145
+ }
146
+
147
+ function classifyDeadCodeExclusion(filePath: string): string | undefined {
148
+ const normalized = filePath.replaceAll('\\', '/');
149
+ if (/(^|\/)__tests__\//.test(normalized) || /\.test\.ts$/.test(normalized) || /\.it\.test\.ts$/.test(normalized)) {
150
+ return 'test';
151
+ }
152
+ if (/(^|\/)fixtures?\//.test(normalized)) return 'fixture';
153
+ if (/(^|\/)(templates|generated)\//.test(normalized)) return 'generated';
154
+ if (/\/(index|main)\.ts$/.test(normalized) || /\/bin\//.test(normalized) || /\/presentation\/(cli|handlers)\//.test(normalized)) {
155
+ return 'entrypoint';
156
+ }
157
+ return undefined;
158
+ }
159
+
160
+ function detectUnusedExports(
161
+ nodes: readonly { filePath: string; exports: readonly string[]; excludedReason?: string }[],
162
+ edges: readonly { to: string; importedNames: readonly string[] }[],
163
+ ): string[] {
164
+ const usedByFile = new Map<string, Set<string>>();
165
+ for (const edge of edges) {
166
+ const names = usedByFile.get(edge.to) ?? new Set<string>();
167
+ for (const importedName of edge.importedNames) {
168
+ names.add(importedName);
169
+ }
170
+ usedByFile.set(edge.to, names);
171
+ }
172
+
173
+ const unused: string[] = [];
174
+ for (const node of nodes) {
175
+ if (node.excludedReason) continue;
176
+ const used = usedByFile.get(normalize(node.filePath)) ?? new Set<string>();
177
+ for (const exportName of node.exports) {
178
+ if (used.has('*') || used.has(exportName)) continue;
179
+ unused.push(`${node.filePath}::${exportName} (reason: no import/export graph reference)`);
180
+ }
181
+ }
182
+ return unused;
183
+ }