1
0

Split meta architecture contracts

This commit is contained in:
2026-03-19 18:52:36 +01:00
parent ac9cc4955a
commit b9f566c49f
12 changed files with 447 additions and 411 deletions

View File

@@ -74,8 +74,12 @@
</testsuite>
<testsuite name="ArchitectureMeta">
<file>tests/Architecture/ApiSchemaContractTest.php</file>
<file>tests/Architecture/AgentSystemContractTest.php</file>
<file>tests/Architecture/CodexSkillsContractTest.php</file>
<file>tests/Architecture/AgentSystemCatalogContractTest.php</file>
<file>tests/Architecture/AgentSystemFileContractTest.php</file>
<file>tests/Architecture/AgentSystemReferenceContractTest.php</file>
<file>tests/Architecture/CodexAutomationContractTest.php</file>
<file>tests/Architecture/CodexDocsIntegrityContractTest.php</file>
<file>tests/Architecture/CodexSkillReferenceContractTest.php</file>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,64 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AgentSystemCatalogContractTest extends TestCase
{
use AgentSystemContractSupport;
use ProjectFileAssertionSupport;
public function testGuardCatalogIdsAreUniqueAndWellFormed(): void
{
$catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json');
$guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [];
$this->assertNotSame([], $guards, 'Guard catalog must not be empty.');
$ids = [];
foreach ($guards as $guard) {
$id = (string) ($guard['id'] ?? '');
$reviewer = (string) ($guard['reviewer'] ?? '');
$this->assertMatchesRegularExpression('/^GR-(CORE|TEST|UI|SEC)-[A-Z0-9]{3,}$/', $id, 'Invalid guard id: ' . $id);
$this->assertContains($reviewer, ['code', 'security'], 'Invalid reviewer for ' . $id);
$ids[] = $id;
}
$this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate guard IDs found.');
}
public function testGuardCatalogReviewerAssignment(): void
{
$catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json');
$guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [];
foreach ($guards as $guard) {
$id = (string) ($guard['id'] ?? '');
$reviewer = (string) ($guard['reviewer'] ?? '');
if (str_starts_with($id, 'GR-SEC-')) {
$this->assertSame('security', $reviewer, $id . ' must be assigned to security reviewer');
} else {
$this->assertSame('code', $reviewer, $id . ' must be assigned to code reviewer');
}
}
}
public function testQualityGateIdsAreUniqueAndWellFormed(): void
{
$gates = $this->decodeAgentJson('.agents/checks/quality-gates.json');
$items = is_array($gates['gates'] ?? null) ? $gates['gates'] : [];
$this->assertNotSame([], $items, 'Quality gates must not be empty.');
$ids = [];
foreach ($items as $gate) {
$id = (string) ($gate['id'] ?? '');
$command = (string) ($gate['command'] ?? '');
$this->assertMatchesRegularExpression('/^QG-[0-9]{3}$/', $id, 'Invalid quality gate id: ' . $id);
$this->assertNotSame('', trim($command), 'Missing command for ' . $id);
$ids[] = $id;
}
$this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate quality gate IDs found.');
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace MintyPHP\Tests\Architecture;
trait AgentSystemContractSupport
{
/**
* @return array<string,mixed>
*/
private function decodeAgentJson(string $path): array
{
$decoded = json_decode($this->readProjectFile($path), true);
$this->assertIsArray($decoded, 'Invalid JSON in ' . $path);
return $decoded;
}
}

View File

@@ -1,198 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AgentSystemContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testAgentSystemJsonFilesAreValidJson(): void
{
$jsonFiles = [
'.agents/checks/guard-catalog.json',
'.agents/checks/quality-gates.json',
'.agents/contracts/analyst.schema.json',
'.agents/contracts/planner.schema.json',
'.agents/contracts/executor.schema.json',
'.agents/contracts/reviewer-code.schema.json',
'.agents/contracts/reviewer-security.schema.json',
'.agents/contracts/reviewer-acceptance.schema.json',
'.agents/contracts/finalizer.schema.json',
'.agents/templates/analysis.template.json',
'.agents/templates/plan.template.json',
'.agents/templates/execution-report.template.json',
'.agents/templates/review-code.template.json',
'.agents/templates/review-security.template.json',
'.agents/templates/review-acceptance.template.json',
'.agents/templates/finalize.template.json',
'.agents/contracts/module-manifest.schema.json',
];
foreach ($jsonFiles as $file) {
$decoded = json_decode($this->readProjectFile($file), true);
$this->assertIsArray($decoded, 'Invalid JSON: ' . $file);
}
}
public function testGuardCatalogIdsAreUniqueAndWellFormed(): void
{
$catalog = $this->decodeJson('.agents/checks/guard-catalog.json');
$guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [];
$this->assertNotSame([], $guards, 'Guard catalog must not be empty.');
$ids = [];
foreach ($guards as $guard) {
$id = (string) ($guard['id'] ?? '');
$reviewer = (string) ($guard['reviewer'] ?? '');
$this->assertMatchesRegularExpression('/^GR-(CORE|TEST|UI|SEC)-[A-Z0-9]{3,}$/', $id, 'Invalid guard id: ' . $id);
$this->assertContains($reviewer, ['code', 'security'], 'Invalid reviewer for ' . $id);
$ids[] = $id;
}
$this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate guard IDs found.');
}
public function testGuardCatalogReviewerAssignment(): void
{
$catalog = $this->decodeJson('.agents/checks/guard-catalog.json');
$guards = is_array($catalog['guards'] ?? null) ? $catalog['guards'] : [];
foreach ($guards as $guard) {
$id = (string) ($guard['id'] ?? '');
$reviewer = (string) ($guard['reviewer'] ?? '');
if (str_starts_with($id, 'GR-SEC-')) {
$this->assertSame('security', $reviewer, $id . ' must be assigned to security reviewer');
} else {
$this->assertSame('code', $reviewer, $id . ' must be assigned to code reviewer');
}
}
}
public function testQualityGateIdsAreUniqueAndWellFormed(): void
{
$gates = $this->decodeJson('.agents/checks/quality-gates.json');
$items = is_array($gates['gates'] ?? null) ? $gates['gates'] : [];
$this->assertNotSame([], $items, 'Quality gates must not be empty.');
$ids = [];
foreach ($items as $gate) {
$id = (string) ($gate['id'] ?? '');
$command = (string) ($gate['command'] ?? '');
$this->assertMatchesRegularExpression('/^QG-[0-9]{3}$/', $id, 'Invalid quality gate id: ' . $id);
$this->assertNotSame('', trim($command), 'Missing command for ' . $id);
$ids[] = $id;
}
$this->assertSame($ids, array_values(array_unique($ids)), 'Duplicate quality gate IDs found.');
}
public function testTemplatesReferenceKnownGuardAndGateIds(): void
{
$catalog = $this->decodeJson('.agents/checks/guard-catalog.json');
$gates = $this->decodeJson('.agents/checks/quality-gates.json');
$guardIds = array_map(
static fn (array $guard): string => (string) ($guard['id'] ?? ''),
is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []
);
$gateIds = array_map(
static fn (array $gate): string => (string) ($gate['id'] ?? ''),
is_array($gates['gates'] ?? null) ? $gates['gates'] : []
);
$planTemplate = $this->decodeJson('.agents/templates/plan.template.json');
$planGuardIds = is_array($planTemplate['guardrails']['required_guard_ids'] ?? null)
? $planTemplate['guardrails']['required_guard_ids']
: [];
$planGateIds = is_array($planTemplate['guardrails']['required_quality_gate_ids'] ?? null)
? $planTemplate['guardrails']['required_quality_gate_ids']
: [];
foreach ($planGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in plan template: ' . (string) $id);
}
foreach ($planGateIds as $id) {
$this->assertContains($id, $gateIds, 'Unknown gate id in plan template: ' . (string) $id);
}
$reviewCodeTemplate = $this->decodeJson('.agents/templates/review-code.template.json');
$reviewGuardIds = is_array($reviewCodeTemplate['checked_guard_ids'] ?? null)
? $reviewCodeTemplate['checked_guard_ids']
: [];
$reviewGateIds = is_array($reviewCodeTemplate['checked_quality_gate_ids'] ?? null)
? $reviewCodeTemplate['checked_quality_gate_ids']
: [];
foreach ($reviewGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in review-code template: ' . (string) $id);
}
foreach ($reviewGateIds as $id) {
$this->assertContains($id, $gateIds, 'Unknown gate id in review-code template: ' . (string) $id);
}
$reviewSecurityTemplate = $this->decodeJson('.agents/templates/review-security.template.json');
$secGuardIds = is_array($reviewSecurityTemplate['checked_guard_ids'] ?? null)
? $reviewSecurityTemplate['checked_guard_ids']
: [];
foreach ($secGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in review-security template: ' . (string) $id);
}
}
public function testRolePromptsReferenceGuardCatalog(): void
{
$plannerPrompt = $this->readProjectFile('.agents/prompts/planner.md');
$executorPrompt = $this->readProjectFile('.agents/prompts/executor.md');
$codeReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-code.md');
$securityReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-security.md');
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $plannerPrompt);
$this->assertStringContainsString('.agents/checks/quality-gates.json', $plannerPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $executorPrompt);
$this->assertStringContainsString('.agents/checks/quality-gates.json', $executorPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $codeReviewerPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $securityReviewerPrompt);
}
public function testAllSevenRolePromptsExist(): void
{
$roles = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer'];
foreach ($roles as $role) {
$this->readProjectFile('.agents/prompts/' . $role . '.md');
}
}
public function testAllSevenContractsExist(): void
{
$contracts = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer'];
foreach ($contracts as $contract) {
$decoded = json_decode($this->readProjectFile('.agents/contracts/' . $contract . '.schema.json'), true);
$this->assertIsArray($decoded, 'Invalid JSON schema: ' . $contract);
}
}
public function testAllSevenTemplatesExist(): void
{
$templates = [
'analysis', 'plan', 'execution-report',
'review-code', 'review-security', 'review-acceptance', 'finalize',
];
foreach ($templates as $template) {
$decoded = json_decode($this->readProjectFile('.agents/templates/' . $template . '.template.json'), true);
$this->assertIsArray($decoded, 'Invalid JSON template: ' . $template);
}
}
/**
* @return array<string,mixed>
*/
private function decodeJson(string $path): array
{
$decoded = json_decode($this->readProjectFile($path), true);
$this->assertIsArray($decoded, 'Invalid JSON in ' . $path);
return $decoded;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AgentSystemFileContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testAgentSystemJsonFilesAreValidJson(): void
{
$jsonFiles = [
'.agents/checks/guard-catalog.json',
'.agents/checks/quality-gates.json',
'.agents/contracts/analyst.schema.json',
'.agents/contracts/planner.schema.json',
'.agents/contracts/executor.schema.json',
'.agents/contracts/reviewer-code.schema.json',
'.agents/contracts/reviewer-security.schema.json',
'.agents/contracts/reviewer-acceptance.schema.json',
'.agents/contracts/finalizer.schema.json',
'.agents/templates/analysis.template.json',
'.agents/templates/plan.template.json',
'.agents/templates/execution-report.template.json',
'.agents/templates/review-code.template.json',
'.agents/templates/review-security.template.json',
'.agents/templates/review-acceptance.template.json',
'.agents/templates/finalize.template.json',
'.agents/contracts/module-manifest.schema.json',
];
foreach ($jsonFiles as $file) {
$decoded = json_decode($this->readProjectFile($file), true);
$this->assertIsArray($decoded, 'Invalid JSON: ' . $file);
}
}
public function testAllSevenRolePromptsExist(): void
{
$roles = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer'];
foreach ($roles as $role) {
$this->readProjectFile('.agents/prompts/' . $role . '.md');
}
}
public function testAllSevenContractsExist(): void
{
$contracts = ['analyst', 'planner', 'executor', 'reviewer-code', 'reviewer-security', 'reviewer-acceptance', 'finalizer'];
foreach ($contracts as $contract) {
$decoded = json_decode($this->readProjectFile('.agents/contracts/' . $contract . '.schema.json'), true);
$this->assertIsArray($decoded, 'Invalid JSON schema: ' . $contract);
}
}
public function testAllSevenTemplatesExist(): void
{
$templates = [
'analysis', 'plan', 'execution-report',
'review-code', 'review-security', 'review-acceptance', 'finalize',
];
foreach ($templates as $template) {
$decoded = json_decode($this->readProjectFile('.agents/templates/' . $template . '.template.json'), true);
$this->assertIsArray($decoded, 'Invalid JSON template: ' . $template);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class AgentSystemReferenceContractTest extends TestCase
{
use AgentSystemContractSupport;
use ProjectFileAssertionSupport;
public function testTemplatesReferenceKnownGuardAndGateIds(): void
{
$catalog = $this->decodeAgentJson('.agents/checks/guard-catalog.json');
$gates = $this->decodeAgentJson('.agents/checks/quality-gates.json');
$guardIds = array_map(
static fn (array $guard): string => (string) ($guard['id'] ?? ''),
is_array($catalog['guards'] ?? null) ? $catalog['guards'] : []
);
$gateIds = array_map(
static fn (array $gate): string => (string) ($gate['id'] ?? ''),
is_array($gates['gates'] ?? null) ? $gates['gates'] : []
);
$planTemplate = $this->decodeAgentJson('.agents/templates/plan.template.json');
$planGuardIds = is_array($planTemplate['guardrails']['required_guard_ids'] ?? null)
? $planTemplate['guardrails']['required_guard_ids']
: [];
$planGateIds = is_array($planTemplate['guardrails']['required_quality_gate_ids'] ?? null)
? $planTemplate['guardrails']['required_quality_gate_ids']
: [];
foreach ($planGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in plan template: ' . (string) $id);
}
foreach ($planGateIds as $id) {
$this->assertContains($id, $gateIds, 'Unknown gate id in plan template: ' . (string) $id);
}
$reviewCodeTemplate = $this->decodeAgentJson('.agents/templates/review-code.template.json');
$reviewGuardIds = is_array($reviewCodeTemplate['checked_guard_ids'] ?? null)
? $reviewCodeTemplate['checked_guard_ids']
: [];
$reviewGateIds = is_array($reviewCodeTemplate['checked_quality_gate_ids'] ?? null)
? $reviewCodeTemplate['checked_quality_gate_ids']
: [];
foreach ($reviewGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in review-code template: ' . (string) $id);
}
foreach ($reviewGateIds as $id) {
$this->assertContains($id, $gateIds, 'Unknown gate id in review-code template: ' . (string) $id);
}
$reviewSecurityTemplate = $this->decodeAgentJson('.agents/templates/review-security.template.json');
$secGuardIds = is_array($reviewSecurityTemplate['checked_guard_ids'] ?? null)
? $reviewSecurityTemplate['checked_guard_ids']
: [];
foreach ($secGuardIds as $id) {
$this->assertContains($id, $guardIds, 'Unknown guard id in review-security template: ' . (string) $id);
}
}
public function testRolePromptsReferenceGuardCatalog(): void
{
$plannerPrompt = $this->readProjectFile('.agents/prompts/planner.md');
$executorPrompt = $this->readProjectFile('.agents/prompts/executor.md');
$codeReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-code.md');
$securityReviewerPrompt = $this->readProjectFile('.agents/prompts/reviewer-security.md');
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $plannerPrompt);
$this->assertStringContainsString('.agents/checks/quality-gates.json', $plannerPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $executorPrompt);
$this->assertStringContainsString('.agents/checks/quality-gates.json', $executorPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $codeReviewerPrompt);
$this->assertStringContainsString('.agents/checks/guard-catalog.json', $securityReviewerPrompt);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class CodexAutomationContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testCodexSkillsSyncScriptSupportsCheckAndApply(): void
{
$script = $this->readProjectFile('bin/codex-skills-sync.sh');
$this->assertStringContainsString('--check', $script);
$this->assertStringContainsString('--apply', $script);
$this->assertStringContainsString('mode="check"', $script);
$this->assertStringContainsString('"core-guardrails"', $script);
$this->assertStringContainsString('"starterkit-planner"', $script);
$this->assertStringContainsString('"starterkit-grid-standards"', $script);
$this->assertStringContainsString('"starterkit-php-style-ci"', $script);
}
public function testDocsLinkCheckScriptExcludesHistoricalRunArtifactsByDefault(): void
{
$script = $this->readProjectFile('bin/docs-link-check.sh');
$this->assertStringContainsString("!.agents/runs/**", $script);
}
public function testQualityGatesIncludeDocsLinksAndSkillsSyncChecks(): void
{
$qualityGates = json_decode($this->readProjectFile('.agents/checks/quality-gates.json'), true);
$this->assertIsArray($qualityGates, 'Invalid quality-gates.json');
$gates = is_array($qualityGates['gates'] ?? null) ? $qualityGates['gates'] : [];
$byId = [];
foreach ($gates as $gate) {
$id = (string) ($gate['id'] ?? '');
$byId[$id] = $gate;
}
$this->assertArrayHasKey('QG-008', $byId);
$this->assertArrayHasKey('QG-009', $byId);
$this->assertSame('bin/docs-link-check.sh', (string) ($byId['QG-008']['command'] ?? ''));
$this->assertSame('bin/codex-skills-sync.sh --check', (string) ($byId['QG-009']['command'] ?? ''));
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace MintyPHP\Tests\Architecture;
trait CodexDocsContractSupport
{
/**
* @return list<string>
*/
private function filesCoveredByDocsLinkIntegrityCheck(): array
{
$root = dirname(__DIR__, 2);
$files = ['README.md'];
$scanDirs = ['docs', '.agents'];
foreach ($scanDirs as $scanDir) {
$directory = new \RecursiveDirectoryIterator(
$root . '/' . $scanDir,
\FilesystemIterator::SKIP_DOTS
);
$iterator = new \RecursiveIteratorIterator($directory);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$fullPath = str_replace('\\', '/', $fileInfo->getPathname());
if (!str_ends_with($fullPath, '.md')) {
continue;
}
$relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $fullPath), '/');
if (str_starts_with($relative, '.agents/runs/')) {
continue;
}
$files[] = $relative;
}
}
$files = array_values(array_unique($files));
sort($files);
return $files;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class CodexDocsIntegrityContractTest extends TestCase
{
use CodexDocsContractSupport;
use ProjectFileAssertionSupport;
public function testCodexPromptsDocumentationReferencesBothSkills(): void
{
$prompts = $this->readProjectFile('docs/reference-codex-prompts.md');
$this->assertStringContainsString('core-guardrails', $prompts);
$this->assertStringContainsString('starterkit-planner', $prompts);
}
public function testDocsIndexReferencesCodexPrompts(): void
{
$index = $this->readProjectFile('docs/index.md');
$this->assertStringContainsString('/docs/reference-codex-prompts.md', $index);
}
public function testDocumentationLinksResolveAcrossDocsSkillsAndAgentGuides(): void
{
foreach ($this->filesCoveredByDocsLinkIntegrityCheck() as $file) {
$content = $this->readProjectFile($file);
preg_match_all('/(?<![A-Za-z0-9_])\\/?docs\\/[a-z0-9_\\/-]+\\.md\\b/', $content, $matches);
foreach (array_unique($matches[0]) as $link) {
$normalized = ltrim($link, '/');
$absolute = dirname(__DIR__, 2) . '/' . $normalized;
$this->assertFileExists($absolute, sprintf('Broken docs link "%s" in %s', $link, $file));
}
}
}
public function testDocsIndexCoversAllMarkdownDocsWithoutOrphansOrDuplicates(): void
{
$index = $this->readProjectFile('docs/index.md');
preg_match_all('/\\/docs\\/([a-z0-9-]+\\.md)\\b/', $index, $matches);
$references = $matches[1];
$docFiles = glob(dirname(__DIR__, 2) . '/docs/*.md');
$this->assertNotFalse($docFiles, 'Failed to enumerate docs/*.md files.');
$expectedDocs = [];
foreach ($docFiles as $fullPath) {
$basename = basename($fullPath);
if ($basename !== 'index.md') {
$expectedDocs[] = $basename;
}
}
$refCounts = array_count_values($references);
$duplicates = [];
foreach ($refCounts as $ref => $count) {
if ($count > 1) {
$duplicates[] = $ref;
}
}
$uniqueReferences = array_values(array_unique($references));
sort($expectedDocs);
sort($uniqueReferences);
sort($duplicates);
$orphans = array_values(array_diff($expectedDocs, $uniqueReferences));
$missingTargets = array_values(array_diff($uniqueReferences, $expectedDocs));
$this->assertSame([], $duplicates, 'Duplicate docs/index.md references: ' . implode(', ', $duplicates));
$this->assertSame([], $orphans, 'Docs markdown files missing in docs/index.md: ' . implode(', ', $orphans));
$this->assertSame([], $missingTargets, 'docs/index.md references missing files: ' . implode(', ', $missingTargets));
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class CodexSkillReferenceContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testCoreGuardrailsSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('.agents/skills/core-guardrails/SKILL.md');
$this->assertStringContainsString('name: core-guardrails', $skill);
$this->readProjectFile('.agents/skills/core-guardrails/references/boundaries-core.md');
$this->readProjectFile('.agents/skills/core-guardrails/references/boundaries-ui.md');
$this->readProjectFile('.agents/skills/core-guardrails/references/quality-gates.md');
$this->readProjectFile('.agents/skills/core-guardrails/references/source-map.md');
}
public function testStarterkitPlannerSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('.agents/skills/starterkit-planner/SKILL.md');
$this->assertStringContainsString('name: starterkit-planner', $skill);
$this->readProjectFile('.agents/skills/starterkit-planner/references/plan-template.md');
$this->readProjectFile('.agents/skills/starterkit-planner/references/tradeoff-matrix.md');
}
public function testStarterkitGridStandardsSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('.agents/skills/starterkit-grid-standards/SKILL.md');
$this->assertStringContainsString('name: starterkit-grid-standards', $skill);
$this->readProjectFile('.agents/skills/starterkit-grid-standards/references/list-checklist.md');
$this->readProjectFile('.agents/skills/starterkit-grid-standards/references/row-interaction-standard.md');
$this->readProjectFile('.agents/skills/starterkit-grid-standards/references/page-size-standard.md');
}
public function testStarterkitPhpStyleCiSkillExists(): void
{
$skill = $this->readProjectFile('.agents/skills/starterkit-php-style-ci/SKILL.md');
$this->assertStringContainsString('name: starterkit-php-style-ci', $skill);
}
}

View File

@@ -1,209 +0,0 @@
<?php
namespace MintyPHP\Tests\Architecture;
use PHPUnit\Framework\TestCase;
class CodexSkillsContractTest extends TestCase
{
use ProjectFileAssertionSupport;
public function testCoreGuardrailsSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('tools/codex-skills/core-guardrails/SKILL.md');
$this->assertStringContainsString('name: core-guardrails', $skill);
$this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-core.md');
$this->readProjectFile('tools/codex-skills/core-guardrails/references/boundaries-ui.md');
$this->readProjectFile('tools/codex-skills/core-guardrails/references/quality-gates.md');
$this->readProjectFile('tools/codex-skills/core-guardrails/references/source-map.md');
}
public function testStarterkitPlannerSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('tools/codex-skills/starterkit-planner/SKILL.md');
$this->assertStringContainsString('name: starterkit-planner', $skill);
$this->readProjectFile('tools/codex-skills/starterkit-planner/references/plan-template.md');
$this->readProjectFile('tools/codex-skills/starterkit-planner/references/tradeoff-matrix.md');
}
public function testStarterkitGridStandardsSkillAndReferencesExist(): void
{
$skill = $this->readProjectFile('tools/codex-skills/starterkit-grid-standards/SKILL.md');
$this->assertStringContainsString('name: starterkit-grid-standards', $skill);
$this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/list-checklist.md');
$this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/row-interaction-standard.md');
$this->readProjectFile('tools/codex-skills/starterkit-grid-standards/references/page-size-standard.md');
}
public function testStarterkitPhpStyleCiSkillExists(): void
{
$skill = $this->readProjectFile('tools/codex-skills/starterkit-php-style-ci/SKILL.md');
$this->assertStringContainsString('name: starterkit-php-style-ci', $skill);
}
public function testCodexPromptsDocumentationReferencesBothSkills(): void
{
$prompts = $this->readProjectFile('docs/reference-codex-prompts.md');
$this->assertStringContainsString('core-guardrails', $prompts);
$this->assertStringContainsString('starterkit-planner', $prompts);
}
public function testDocsIndexReferencesCodexPrompts(): void
{
$index = $this->readProjectFile('docs/index.md');
$this->assertStringContainsString('/docs/reference-codex-prompts.md', $index);
}
public function testCodexSkillsSyncScriptSupportsCheckAndApply(): void
{
$script = $this->readProjectFile('bin/codex-skills-sync.sh');
$this->assertStringContainsString('--check', $script);
$this->assertStringContainsString('--apply', $script);
$this->assertStringContainsString('mode="check"', $script);
$this->assertStringContainsString('"core-guardrails"', $script);
$this->assertStringContainsString('"starterkit-planner"', $script);
$this->assertStringContainsString('"starterkit-grid-standards"', $script);
$this->assertStringContainsString('"starterkit-php-style-ci"', $script);
}
public function testDocsLinkCheckScriptExcludesHistoricalRunArtifactsByDefault(): void
{
$script = $this->readProjectFile('bin/docs-link-check.sh');
$this->assertStringContainsString("!agent-system/runs/**", $script);
}
public function testDocumentationLinksResolveAcrossDocsSkillsAndAgentGuides(): void
{
foreach ($this->filesCoveredByDocsLinkIntegrityCheck() as $file) {
$content = $this->readProjectFile($file);
preg_match_all('/(?<![A-Za-z0-9_])\\/?docs\\/[a-z0-9_\\/-]+\\.md\\b/', $content, $matches);
foreach (array_unique($matches[0]) as $link) {
$normalized = ltrim($link, '/');
$absolute = $this->projectRootPath() . '/' . $normalized;
$this->assertFileExists($absolute, sprintf('Broken docs link "%s" in %s', $link, $file));
}
}
}
public function testDocsIndexCoversAllMarkdownDocsWithoutOrphansOrDuplicates(): void
{
$index = $this->readProjectFile('docs/index.md');
preg_match_all('/\\/docs\\/([a-z0-9-]+\\.md)\\b/', $index, $matches);
$references = $matches[1];
$docFiles = glob($this->projectRootPath() . '/docs/*.md');
$this->assertNotFalse($docFiles, 'Failed to enumerate docs/*.md files.');
$expectedDocs = [];
foreach ($docFiles as $fullPath) {
$basename = basename($fullPath);
if ($basename !== 'index.md') {
$expectedDocs[] = $basename;
}
}
$refCounts = array_count_values($references);
$duplicates = [];
foreach ($refCounts as $ref => $count) {
if ($count > 1) {
$duplicates[] = $ref;
}
}
$uniqueReferences = array_values(array_unique($references));
sort($expectedDocs);
sort($uniqueReferences);
sort($duplicates);
$orphans = array_values(array_diff($expectedDocs, $uniqueReferences));
$missingTargets = array_values(array_diff($uniqueReferences, $expectedDocs));
$this->assertSame([], $duplicates, 'Duplicate docs/index.md references: ' . implode(', ', $duplicates));
$this->assertSame([], $orphans, 'Docs markdown files missing in docs/index.md: ' . implode(', ', $orphans));
$this->assertSame([], $missingTargets, 'docs/index.md references missing files: ' . implode(', ', $missingTargets));
}
public function testQualityGatesIncludeDocsLinksAndSkillsSyncChecks(): void
{
$qualityGates = json_decode($this->readProjectFile('agent-system/checks/quality-gates.json'), true);
$this->assertIsArray($qualityGates, 'Invalid quality-gates.json');
$gates = is_array($qualityGates['gates'] ?? null) ? $qualityGates['gates'] : [];
$byId = [];
foreach ($gates as $gate) {
$id = (string) ($gate['id'] ?? '');
$byId[$id] = $gate;
}
$this->assertArrayHasKey('QG-008', $byId);
$this->assertArrayHasKey('QG-009', $byId);
$this->assertSame('bin/docs-link-check.sh', (string) ($byId['QG-008']['command'] ?? ''));
$this->assertSame('bin/codex-skills-sync.sh --check', (string) ($byId['QG-009']['command'] ?? ''));
}
public function testDocsSkillsAuditPolicyExists(): void
{
$policy = $this->readProjectFile('agent-system/checks/docs-skills-audit-policy.md');
$this->assertStringContainsString('P0', $policy);
$this->assertStringContainsString('P1', $policy);
$this->assertStringContainsString('DOCS-SKILLS-AUDIT-00x', $policy);
}
public function testReviewerGuardsFlowEnforcesDocsSkillsAuditPolicy(): void
{
$prompt = $this->readProjectFile('agent-system/prompts/reviewer-guards.md');
$schema = $this->readProjectFile('agent-system/contracts/reviewer-guards.schema.json');
$this->assertStringContainsString('agent-system/checks/docs-skills-audit-policy.md', $prompt);
$this->assertStringContainsString('DOCS-SKILLS-AUDIT', $prompt);
$this->assertStringContainsString('audit_policy_ref', $schema);
$this->assertStringContainsString('"P0"', $schema);
$this->assertStringContainsString('"P1"', $schema);
$this->assertStringContainsString('run_artifact_id', $schema);
$this->assertStringContainsString('DOCS-SKILLS-AUDIT-[0-9]{3}', $schema);
}
/**
* @return list<string>
*/
private function filesCoveredByDocsLinkIntegrityCheck(): array
{
$root = $this->projectRootPath();
$files = ['README.md'];
$scanDirs = ['docs', 'tools/codex-skills', 'agent-system'];
foreach ($scanDirs as $scanDir) {
$directory = new \RecursiveDirectoryIterator(
$root . '/' . $scanDir,
\FilesystemIterator::SKIP_DOTS
);
$iterator = new \RecursiveIteratorIterator($directory);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$fullPath = str_replace('\\', '/', $fileInfo->getPathname());
if (!str_ends_with($fullPath, '.md')) {
continue;
}
$relative = ltrim(str_replace(str_replace('\\', '/', $root), '', $fullPath), '/');
if (str_starts_with($relative, 'agent-system/runs/')) {
continue;
}
$files[] = $relative;
}
}
$files = array_values(array_unique($files));
sort($files);
return $files;
}
}

View File

@@ -32,8 +32,8 @@ ohne dass jede Aenderung immer die komplette Suite im Kopf behalten muss.
## Naechste Refactor-Kandidaten
- `tests/Architecture/CodexSkillsContractTest.php`
- `tests/Architecture/ModuleRegistryContractTest.php`
- `tests/Architecture/AgentSystemContractTest.php`
- `tests/Architecture/DetailValidationSummaryContractTest.php`
- `tests/Architecture/DetailActionPolicyContractTest.php`
- `tests/Architecture/NoAddressBookHardcodingTest.php`
- `tests/Architecture/NoBookmarksHardcodingTest.php`