decodeAgentJson('.agents/checks/quality-gates.json'); $policy = $this->decodeAgentJson('.agents/checks/enforcement-policy.json'); $knownGateIds = array_map( static fn (array $gate): string => (string) ($gate['id'] ?? ''), is_array($gates['gates'] ?? null) ? $gates['gates'] : [] ); $required = is_array($policy['gate_policy']['required_gates_always'] ?? null) ? $policy['gate_policy']['required_gates_always'] : []; $manual = is_array($policy['gate_policy']['manual_non_blocking'] ?? null) ? $policy['gate_policy']['manual_non_blocking'] : []; $periodic = is_array($policy['gate_policy']['periodic_non_blocking'] ?? null) ? $policy['gate_policy']['periodic_non_blocking'] : []; foreach (array_merge($required, $manual, $periodic) as $gateId) { $this->assertContains($gateId, $knownGateIds, 'Unknown gate id in enforcement-policy.json: ' . (string) $gateId); } } public function testGuardEnforcementMapCoversAllGuardsExactlyOnce(): void { $catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json'); $map = $this->decodeAgentJson('.agents/checks/guard-enforcement-map.json'); $knownGuardIds = array_map( static fn (array $guard): string => (string) ($guard['id'] ?? ''), is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [] ); $entries = is_array($map['entries'] ?? null) ? $map['entries'] : []; $mappedGuardIds = array_map( static fn (array $entry): string => (string) ($entry['guard_id'] ?? ''), $entries ); $this->assertSame($mappedGuardIds, array_values(array_unique($mappedGuardIds)), 'Duplicate guard_id in guard-enforcement-map.json.'); foreach ($knownGuardIds as $guardId) { $this->assertContains($guardId, $mappedGuardIds, 'Missing guard mapping: ' . $guardId); } foreach ($mappedGuardIds as $guardId) { $this->assertContains($guardId, $knownGuardIds, 'Unknown guard_id in guard-enforcement-map.json: ' . $guardId); } foreach ($entries as $entry) { $mode = (string) ($entry['enforcement_mode'] ?? ''); $this->assertContains( $mode, ['automated', 'hybrid', 'reviewer'], 'Invalid enforcement_mode in guard-enforcement-map.json for ' . (string) ($entry['guard_id'] ?? '') ); } } public function testAutomatedGuardMappingsReferenceExistingTestFiles(): void { $map = $this->decodeAgentJson('.agents/checks/guard-enforcement-map.json'); $entries = is_array($map['entries'] ?? null) ? $map['entries'] : []; $root = $this->projectRootPath(); foreach ($entries as $entry) { $guardId = (string) ($entry['guard_id'] ?? ''); $mode = (string) ($entry['enforcement_mode'] ?? ''); if ($mode !== 'automated') { continue; } $sources = is_array($entry['evidence_source'] ?? null) ? $entry['evidence_source'] : []; $testPaths = array_values(array_filter( $sources, static fn ($source): bool => is_string($source) && str_starts_with($source, 'tests/') )); $this->assertNotSame([], $testPaths, "Automated guard {$guardId} must reference at least one test file in evidence_source."); foreach ($testPaths as $testPath) { $this->assertFileExists($root . '/' . $testPath, "Automated guard {$guardId} references missing test file: {$testPath}"); } } } public function testGuardAndGateReferencesAcrossRepoUseKnownIds(): void { $catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json'); $gates = $this->decodeAgentJson('.agents/checks/quality-gates.json'); $knownGuardIds = array_fill_keys(array_map( static fn (array $guard): string => (string) ($guard['id'] ?? ''), is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [] ), true); $knownGateIds = array_fill_keys(array_map( static fn (array $gate): string => (string) ($gate['id'] ?? ''), is_array($gates['gates'] ?? null) ? $gates['gates'] : [] ), true); $unknown = []; foreach ($this->idReferenceScanFiles() as $relativePath) { $content = $this->readProjectFile($relativePath); preg_match_all('/\bGR-(CORE|TEST|UI|SEC)-[A-Z0-9]{3,}\b/', $content, $guardMatches); foreach (array_unique($guardMatches[0] ?? []) as $id) { if (!isset($knownGuardIds[$id])) { $unknown[] = $relativePath . ': ' . $id; } } preg_match_all('/\bQG-[0-9]{3}\b/', $content, $gateMatches); foreach (array_unique($gateMatches[0] ?? []) as $id) { if (!isset($knownGateIds[$id])) { $unknown[] = $relativePath . ': ' . $id; } } } $unknown = array_values(array_unique($unknown)); sort($unknown); $this->assertSame( [], $unknown, "Unknown GR/QG references found in repository:\n" . implode("\n", $unknown) ); } /** * @return list */ private function idReferenceScanFiles(): array { $root = $this->projectRootPath(); $scanTargets = ['CLAUDE.md', '.agents', 'docs', 'tests', 'bin', '.gitea']; $extensions = ['md', 'json', 'php', 'sh', 'yml', 'yaml']; $files = []; foreach ($scanTargets as $target) { $fullPath = $root . '/' . $target; if (is_file($fullPath)) { $files[] = $target; continue; } if (!is_dir($fullPath)) { continue; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($fullPath, \FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if (!$file->isFile()) { continue; } $ext = strtolower((string) $file->getExtension()); if (!in_array($ext, $extensions, true)) { continue; } $relativePath = str_replace($root . '/', '', $file->getPathname()); $files[] = $relativePath; } } $files = array_values(array_unique($files)); sort($files); return $files; } }