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
@@ -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
+ }
@@ -2,8 +2,8 @@
2
2
  // @layer domain
3
3
 
4
4
  export interface ImportGraphData {
5
- readonly nodes: readonly { filePath: string; exports: readonly string[] }[];
6
- readonly edges: readonly { from: string; to: string; importedNames: readonly string[] }[];
5
+ readonly nodes: readonly { filePath: string; exports: readonly string[]; excludedReason?: string }[];
6
+ readonly edges: readonly { from: string; to: string; importedNames: readonly string[]; kind?: string }[];
7
7
  readonly unusedExports?: readonly string[];
8
8
  readonly unreachableCode?: readonly { filePath: string; range: { startLine: number; endLine: number } }[];
9
9
  }
@@ -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
+ }
@@ -9,8 +9,8 @@ import { DeadCodeReport } from '../../value-objects/dead-code-report.js';
9
9
 
10
10
  export interface DeadCodeSourceAnalysisPort {
11
11
  getImportGraph(): Promise<{
12
- nodes?: readonly { filePath: string; exports: readonly string[] }[];
13
- edges?: readonly { from: string; to: string; importedNames: readonly string[] }[];
12
+ nodes?: readonly { filePath: string; exports: readonly string[]; excludedReason?: string }[];
13
+ edges?: readonly { from: string; to: string; importedNames: readonly string[]; kind?: string }[];
14
14
  unusedExports?: readonly string[];
15
15
  unreachableCode?: readonly { filePath: string; range: { startLine: number; endLine: number } }[];
16
16
  }>;
@@ -8,9 +8,11 @@
8
8
  import * as ts from 'typescript';
9
9
  import type { PerformanceScannerPort } from '../../domain/ports/performance-scanner-port.js';
10
10
  import type { HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
11
- import { readdir, stat } from 'node:fs/promises';
11
+ import { readFile, readdir, stat } from 'node:fs/promises';
12
12
  import { join, resolve } from 'node:path';
13
13
 
14
+ const SUPPRESSION_MARKER = 'phasegate-ignore-performance';
15
+
14
16
  export class AstPerformanceScannerAdapter implements PerformanceScannerPort {
15
17
  async scan(targetPaths: readonly string[], thresholds: Record<string, number>): Promise<{
16
18
  passed: boolean;
@@ -33,12 +35,15 @@ export class AstPerformanceScannerAdapter implements PerformanceScannerPort {
33
35
 
34
36
  for (const filePath of filePaths) {
35
37
  try {
36
- const [fileStat] = await Promise.all([stat(filePath)]);
38
+ const [fileStat, content] = await Promise.all([stat(filePath), readFile(filePath, 'utf8')]);
39
+ if (content.includes(SUPPRESSION_MARKER)) {
40
+ continue;
41
+ }
37
42
 
38
43
  // Bundle size check (file-level size)
39
44
  const bundleSizeLimit = thresholds.bundleSizeLimit;
40
45
  if (typeof bundleSizeLimit === 'number' && fileStat.size > bundleSizeLimit) {
41
- findings.push(createFinding('L3-002', `bundleSizeLimit を超過しました: ${filePath} (${fileStat.size} bytes)`));
46
+ findings.push(createFinding('L3-002', `bundleSizeLimit を超過しました: ${filePath} metric=file-size actual=${fileStat.size} threshold=${bundleSizeLimit}`));
42
47
  }
43
48
 
44
49
  // AST-based await-in-loop detection
@@ -46,6 +51,13 @@ export class AstPerformanceScannerAdapter implements PerformanceScannerPort {
46
51
  if (sourceFile && hasAwaitInLoop(sourceFile)) {
47
52
  findings.push(createFinding('L3-002', `ループ内 await を検出しました: ${filePath}`));
48
53
  }
54
+ if (sourceFile && hasSyncIoCall(sourceFile)) {
55
+ findings.push(createFinding('L3-002', `同期I/O呼び出しを検出しました: ${filePath} metric=sync-io threshold=0`));
56
+ }
57
+ const largeLiteralThreshold = thresholds.largeLiteralEntries ?? 80;
58
+ if (sourceFile && hasLargeLiteral(sourceFile, largeLiteralThreshold)) {
59
+ findings.push(createFinding('L3-002', `largeLiteralEntries を超過しました: ${filePath} metric=literal-entries threshold=${largeLiteralThreshold}`));
60
+ }
49
61
  } catch {
50
62
  continue;
51
63
  }
@@ -56,6 +68,46 @@ export class AstPerformanceScannerAdapter implements PerformanceScannerPort {
56
68
  }
57
69
  }
58
70
 
71
+ function hasSyncIoCall(root: ts.Node): boolean {
72
+ let found = false;
73
+
74
+ function visit(node: ts.Node): void {
75
+ if (found) return;
76
+ if (ts.isCallExpression(node)) {
77
+ const expressionText = node.expression.getText(root.getSourceFile());
78
+ if (/\b(?:readFileSync|writeFileSync|appendFileSync|readdirSync|statSync|existsSync|execFileSync|execSync|spawnSync)\b/.test(expressionText)) {
79
+ found = true;
80
+ return;
81
+ }
82
+ }
83
+ ts.forEachChild(node, visit);
84
+ }
85
+
86
+ visit(root);
87
+ return found;
88
+ }
89
+
90
+ function hasLargeLiteral(root: ts.Node, threshold: number): boolean {
91
+ let found = false;
92
+
93
+ function visit(node: ts.Node): void {
94
+ if (found) return;
95
+ const literalSize = ts.isObjectLiteralExpression(node)
96
+ ? node.properties.length
97
+ : ts.isArrayLiteralExpression(node)
98
+ ? node.elements.length
99
+ : 0;
100
+ if (literalSize > threshold) {
101
+ found = true;
102
+ return;
103
+ }
104
+ ts.forEachChild(node, visit);
105
+ }
106
+
107
+ visit(root);
108
+ return found;
109
+ }
110
+
59
111
  /**
60
112
  * ループノード(for/while/do/for-in/for-of)の直下に await が存在するか検出する。
61
113
  * ネストされた関数・アロー関数境界は越えない。
@@ -131,8 +131,11 @@ function extractExports(sourceFile: ts.SourceFile): SourceAnalysisResult['export
131
131
  exports.push({ name: element.name.text, type: 'type' });
132
132
  }
133
133
  }
134
- } else if (ts.isExportDeclaration(node) && !node.exportClause && ts.isStringLiteral(node.moduleSpecifier)) {
135
- exports.push({ name: `* from ${node.moduleSpecifier.text}`, type: 'type' });
134
+ } else if (ts.isExportDeclaration(node) && !node.exportClause) {
135
+ const moduleSpecifier = node.moduleSpecifier;
136
+ if (moduleSpecifier !== undefined && ts.isStringLiteral(moduleSpecifier)) {
137
+ exports.push({ name: `* from ${moduleSpecifier.text}`, type: 'type' });
138
+ }
136
139
  }
137
140
 
138
141
  const hasDefault = modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
@@ -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
+ }
@@ -8,11 +8,26 @@ import type { SecurityPatternScannerPort } from '../../domain/ports/security-pat
8
8
  import type { HarnessErrorLike } from '../../domain/value-objects/validation-result.js';
9
9
  import { readFile } from 'node:fs/promises';
10
10
 
11
- const SECURITY_PATTERNS = [
12
- { pattern: /(?:API_KEY|api_key|apikey)\s*=\s*["'][^"']{8,}["']/gi, description: 'ハードコードAPIキー' },
13
- { pattern: /(?:password|PASSWORD|passwd)\s*=\s*["'][^"']{4,}["']/gi, description: 'ハードコードパスワード' },
14
- { pattern: /sk-[a-zA-Z0-9]{20,}/g, description: 'OpenAI APIキー形式' },
15
- ];
11
+ const ALLOWLIST_MARKER = 'phasegate-allow-secret-fixture';
12
+
13
+ interface SecurityPattern {
14
+ readonly ruleId: string;
15
+ readonly pattern: RegExp;
16
+ readonly description: string;
17
+ }
18
+
19
+ const SECURITY_PATTERNS: readonly SecurityPattern[] = Object.freeze([
20
+ { ruleId: 'secret.openai', pattern: /\b(?:sk|rk|sess)-[a-zA-Z0-9_-]{20,}\b/g, description: 'OpenAI token family' },
21
+ { ruleId: 'secret.github', pattern: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g, description: 'GitHub token family' },
22
+ { ruleId: 'secret.aws-access-key', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, description: 'AWS access key id' },
23
+ { ruleId: 'secret.npm', pattern: /\bnpm_[A-Za-z0-9]{24,}\b/g, description: 'npm token family' },
24
+ { ruleId: 'secret.slack', pattern: /\bxox[abprs]-[A-Za-z0-9-]{20,}\b/g, description: 'Slack token family' },
25
+ {
26
+ ruleId: 'secret.keyword-context',
27
+ pattern: /\b(?:API_KEY|api_key|apikey|password|PASSWORD|passwd|secret|token)\b\s*[:=]\s*["'][^"']{8,}["']/g,
28
+ description: 'keyword-context secret',
29
+ },
30
+ ]);
16
31
 
17
32
  export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternScannerPort {
18
33
  async scan(targetPaths: readonly string[]): Promise<{
@@ -24,15 +39,24 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
24
39
  for (const filePath of targetPaths) {
25
40
  try {
26
41
  const content = await readFile(filePath, 'utf-8');
42
+ if (content.includes(ALLOWLIST_MARKER)) {
43
+ continue;
44
+ }
27
45
  const lines = content.split('\n');
28
46
  lines.forEach((line, idx) => {
29
- for (const { pattern, description } of SECURITY_PATTERNS) {
30
- if (pattern.test(line)) {
47
+ if (isAllowlisted(line, content)) {
48
+ return;
49
+ }
50
+ for (const { pattern, description, ruleId } of SECURITY_PATTERNS) {
51
+ pattern.lastIndex = 0;
52
+ const matches = [...line.matchAll(pattern)];
53
+ for (const match of matches) {
54
+ const secretValue = match[0] ?? '';
31
55
  findings.push({
32
56
  code: { value: 'L3-001', toString: () => 'L3-001' },
33
57
  severity: { value: 'error', toString: () => 'error' },
34
- message: `セキュリティ問題: ${description} at ${filePath}:${idx + 1}`,
35
- suggestion: '秘密情報は環境変数または秘密管理サービスを使用してください',
58
+ message: `セキュリティ問題: ${description} (${ruleId}) at ${filePath}:${idx + 1} value=${redactSecret(secretValue)}`,
59
+ suggestion: `${ruleId}: 秘密情報は環境変数または秘密管理サービスを使用してください。fixture/docs のダミー値は ${ALLOWLIST_MARKER} を明示してください。`,
36
60
  });
37
61
  }
38
62
  }
@@ -45,3 +69,14 @@ export class FileSystemSecurityPatternScannerAdapter implements SecurityPatternS
45
69
  return { passed: findings.length === 0, findings };
46
70
  }
47
71
  }
72
+
73
+ function isAllowlisted(line: string, content: string): boolean {
74
+ if (line.includes(ALLOWLIST_MARKER)) return true;
75
+ return /@example|dummy|placeholder/i.test(line) && content.includes(ALLOWLIST_MARKER);
76
+ }
77
+
78
+ function redactSecret(secretValue: string): string {
79
+ const value = secretValue.trim();
80
+ if (value.length <= 8) return '<redacted>';
81
+ return `${value.slice(0, 3)}...<redacted:${value.length}>`;
82
+ }