# Category Completeness Check

> **Reference for:** step-01-task.md section 4b and step-04-check.md section 1.7
> **Purpose:** Ensure PRD contains tasks for ALL required categories before/during execution
> **Frequency:** On load (step-01) and every iteration (step-04)

---

## The Problem

**Test-v4-005 Failure Mode:**
- PRD generated with only backend categories (domain, infrastructure, application, api, seedData)
- Frontend and test categories were entirely absent
- Result: 0 frontend pages, 0 tests generated across 3 modules
- Root cause: No guardrail detection during load

## The Solution: Required Categories

**CRITICAL:** Every PRD MUST have tasks for these categories:

```javascript
const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
```

> **Why seedData is required:**
> - Without it: Navigation/Permissions are not seeded
> - Result: Frontend/test tasks execute against empty database
> - Guardrail must inject NavigationApplicationSeedData, ApplicationRolesSeedData, etc.

---

## Completeness Check Function

```javascript
function checkCategoryCompleteness(prd) {
  const presentCategories = new Set(prd.tasks.map(t => t.category));
  const REQUIRED_CATEGORIES = ['domain', 'infrastructure', 'application', 'api', 'seedData', 'frontend', 'test'];
  const missingCategories = REQUIRED_CATEGORIES.filter(c => !presentCategories.has(c));

  if (missingCategories.length === 0) {
    return { missing: [], guardrailsNeeded: [] };
  }

  console.warn(`⚠ PRD MISSING CATEGORIES: ${missingCategories.join(', ')}`);

  // Calculate max ID for new guardrails
  let maxIdNum = Math.max(...prd.tasks.map(t => {
    const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
    return isNaN(num) ? 0 : num;
  }), 0);

  const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'GUARD-';
  const lastApiTask = prd.tasks.filter(t => t.category === 'api').pop()?.id;
  const lastSeedDataTask = prd.tasks.filter(t => t.category === 'seedData').pop()?.id;

  // Dependency chain: domain → infrastructure → application → api → seedData → frontend → test
  // Frontend MUST depend on seedData (navigation/permissions must be seeded first)
  const frontendDep = lastSeedDataTask || lastApiTask;

  const guardrailTasks = [];

  for (const cat of missingCategories) {
    maxIdNum++;
    const taskId = `${prefix}${String(maxIdNum).padStart(3, '0')}`;

    // Determine dependencies based on category order
    let deps;
    if (cat === 'seedData') {
      deps = [lastApiTask]; // seedData depends on API infrastructure
    } else if (cat === 'frontend') {
      deps = [frontendDep]; // frontend depends on seedData (navigation must be seeded)
    } else if (cat === 'test') {
      deps = [frontendDep]; // test depends on seedData too
    } else {
      deps = [];
    }

    const guardrailTask = {
      id: taskId,
      description: `[GUARDRAIL] Generate ${cat} layer for ${prd.project?.module || 'module'}`,
      status: 'pending',
      category: cat,
      dependencies: deps,
      acceptance_criteria: determineAcceptanceCriteria(cat),
      started_at: null,
      completed_at: null,
      iteration: null,
      commit_hash: null,
      files_changed: [],
      validation: null,
      error: null
    };

    guardrailTasks.push(guardrailTask);
  }

  return { missing: missingCategories, guardrailsNeeded: guardrailTasks };
}

function determineAcceptanceCriteria(category) {
  const criteria = {
    domain: 'Entity classes created with required properties',
    infrastructure: 'EF Core configs, migrations created, dotnet build passes',
    application: 'Service classes implementing business logic',
    api: 'REST controllers with [RequirePermission] attributes',
    seedData: 'NavigationApplicationSeedData + ApplicationRolesSeedData + PermissionsSeedData created, IClientSeedDataProvider wired, startup test passes',
    frontend: 'React pages created via MCP scaffold_api_client + scaffold_routes, wired to App.tsx, navigation seed data verified in DB',
    test: 'Unit + Integration test projects created, scaffold_tests MCP called, dotnet test passes'
  };
  return criteria[category] || `${category} layer fully implemented per category-rules.md`;
}
```

## Application in step-01

```javascript
// After loading prd.json or generating tasks
const { missing, guardrailsNeeded } = checkCategoryCompleteness(prd);

if (guardrailsNeeded.length > 0) {
  console.log(`PRD updated: ${prd.tasks.length} → ${prd.tasks.length + guardrailsNeeded.length} tasks (${guardrailsNeeded.length} guardrails added)`);
  prd.tasks.push(...guardrailsNeeded);

  // Update frontendDep for any frontend guardrail if seedData was just injected
  if (missing.includes('seedData')) {
    const seedDataGuard = guardrailsNeeded.find(t => t.category === 'seedData');
    const frontendGuard = prd.tasks.find(t => t.category === 'frontend' && t.description.includes('[GUARDRAIL]'));
    if (frontendGuard && seedDataGuard) {
      frontendGuard.dependencies = [seedDataGuard.id];
    }
  }

  writeJSON(currentPrdPath, prd);
}
```

## Application in step-04 (every iteration)

```javascript
// RUNS EVERY ITERATION — not just once
const { missing, guardrailsNeeded } = checkCategoryCompleteness(prd);

if (guardrailsNeeded.length > 0) {
  prd.tasks.push(...guardrailsNeeded);
  writeJSON(currentPrdPath, prd);

  const newPending = prd.tasks.filter(t => t.status === 'pending').length;
  console.log(`PRD updated: +${guardrailsNeeded.length} guardrails, ${newPending} pending tasks`);
}
```

## Artifact Verification

After category check, verify artifacts actually exist for completed tasks.

> **Patterns read from PRD metadata first** (`prd.metadata.artifactPatterns`), falling back to SmartStack defaults.
> This allows non-standard project structures to work without hardcoded paths.

```javascript
const completedCats = new Set(prd.tasks.filter(t => t.status === 'completed').map(t => t.category));

// Read custom patterns from PRD metadata (set by ss business-analyse-handoff based on project structure)
const customPatterns = prd.metadata?.artifactPatterns || {};

const artifactChecks = {
  'frontend': () => {
    const patterns = customPatterns.frontend || ['**/src/pages/**/*.tsx', '**/src/app/**/*.tsx'];
    return patterns.some(p => glob.sync(p).length > 0);
  },
  'test': () => {
    const patterns = customPatterns.test || ['tests/**/*Tests.cs', '**/*.spec.ts', '**/*.test.ts'];
    return patterns.some(p => glob.sync(p).length > 0);
  },
  'api': () => {
    const patterns = customPatterns.api || ['src/*/Controllers/**/*Controller.cs', 'src/**/controllers/**/*.ts'];
    return patterns.some(p => glob.sync(p).length > 0);
  }
};

for (const [cat, check] of Object.entries(artifactChecks)) {
  if (completedCats.has(cat) && !check()) {
    console.warn(`ARTIFACT MISSING: ${cat} tasks marked completed but no files found — resetting to pending`);
    prd.tasks.filter(t => t.category === cat && t.status === 'completed')
      .forEach(t => {
        t.status = 'pending';
        t.error = 'Artifacts missing — re-execute';
        t.completed_at = null;
      });
    writeJSON(currentPrdPath, prd);
  }
}
```

## Entity-Level File Completeness Check (BLOCKING)

> **LESSON LEARNED (audit ba-002):** Artifact verification checked "at least one file per category"
> but never reconciled against the **handoff contract** (`prd.implementation.filesToCreate`).
> Result: 4/17 API endpoints were missing but the category showed "complete" because *some* controllers existed.

```javascript
// BLOCKING: Verify EVERY file from prd.implementation.filesToCreate exists on disk
const filesToCreate = prd.implementation?.filesToCreate;
if (filesToCreate) {
  const handoffMissing = [];
  const handoffPresent = [];

  for (const [category, files] of Object.entries(filesToCreate)) {
    for (const file of (files || [])) {
      const filePath = file.path || file;
      if (fileExists(filePath)) {
        handoffPresent.push({ category, path: filePath });
      } else {
        handoffMissing.push({ category, path: filePath });
      }
    }
  }

  const totalHandoff = handoffPresent.length + handoffMissing.length;
  const coveragePct = totalHandoff > 0 ? Math.round((handoffPresent.length / totalHandoff) * 100) : 100;
  console.log(`Handoff file coverage: ${handoffPresent.length}/${totalHandoff} (${coveragePct}%)`);

  if (handoffMissing.length > 0) {
    console.error(`BLOCKING: ${handoffMissing.length} files from handoff contract missing on disk`);

    // Group missing files by category for targeted remediation
    const missingByCategory = {};
    for (const m of handoffMissing) {
      if (!missingByCategory[m.category]) missingByCategory[m.category] = [];
      missingByCategory[m.category].push(m.path);
    }

    // Inject remediation tasks for each missing file
    let maxIdNum = Math.max(...prd.tasks.map(t => {
      const num = parseInt(t.id.replace(/[^0-9]/g, ''), 10);
      return isNaN(num) ? 0 : num;
    }), 0);
    const prefix = prd.tasks[0]?.id?.replace(/[0-9]+$/, '') || 'HNDOFF-';

    for (const [cat, paths] of Object.entries(missingByCategory)) {
      for (const p of paths) {
        // Skip if a remediation task already exists for this file
        const alreadyExists = prd.tasks.some(t =>
          t.description.includes(p) && t.description.includes('[HANDOFF-REMEDIATION]')
        );
        if (alreadyExists) continue;

        maxIdNum++;
        prd.tasks.push({
          id: `${prefix}${String(maxIdNum).padStart(3, '0')}`,
          description: `[HANDOFF-REMEDIATION] Create missing file: ${p}`,
          status: 'pending',
          category: cat,
          dependencies: [],
          acceptance_criteria: `File ${p} exists on disk and compiles`,
          started_at: null, completed_at: null, iteration: null,
          commit_hash: null, files_changed: [], validation: null, error: null
        });
      }
      console.error(`  ${cat}: ${paths.length} missing — ${paths.map(p => p.split('/').pop()).join(', ')}`);
    }

    writeJSON(currentPrdPath, prd);
  }
} // end filesToCreate check
```

## Type Reference Verification (Cross-File Integrity)

> **LESSON LEARNED (audit ba-002):** Artifact verification counted files but didn't check
> that types referenced in code actually exist. `EmployeeListDto` was used in services
> but the file was missing — a "phantom reference" that artifact file-counting missed.

```javascript
// After artifact checks, verify cross-file type references for completed categories
if (completedCats.has('application') && completedCats.has('api')) {
  const serviceFiles = glob('src/**/Services/**/*Service.cs');
  const dtoFiles = glob('src/**/DTOs/**/*.cs');
  const entityFiles = glob('src/**/Domain/**/*.cs');

  // Extract all DTO type names from actual files
  const existingTypes = new Set();
  for (const f of [...dtoFiles, ...entityFiles]) {
    const content = readFile(f);
    const typeMatches = content.matchAll(/(?:class|record|struct|enum|interface)\s+(\w+)/g);
    for (const m of typeMatches) existingTypes.add(m[1]);
  }

  // Check service files for references to types that don't exist
  const phantomRefs = [];
  for (const f of serviceFiles) {
    const content = readFile(f);
    // Look for Dto/Entity type references in return types and method params
    const typeRefs = content.matchAll(/(?:<|,\s*|\(|new\s+)(\w+(?:Dto|Entity|Exception))\b/g);
    for (const m of typeRefs) {
      if (!existingTypes.has(m[1]) && !m[1].startsWith('I')) {
        phantomRefs.push({ file: f, type: m[1] });
      }
    }
  }

  if (phantomRefs.length > 0) {
    console.error(`PHANTOM TYPE REFERENCES DETECTED (${phantomRefs.length}):`);
    phantomRefs.forEach(r => console.error(`  ${r.file}: references ${r.type} — TYPE DOES NOT EXIST`));

    // Reset application category tasks to pending — code references non-existent types
    prd.tasks.filter(t => t.category === 'application' && t.status === 'completed')
      .forEach(t => {
        t.status = 'pending';
        t.error = `Phantom type references: ${phantomRefs.map(r=>r.type).join(', ')}`;
        t.completed_at = null;
      });
    writeJSON(currentPrdPath, prd);
  }
}
```

---

## Key Rules

- **Inject EVERY iteration:** Not just once during load
- **Frontend depends on seedData:** Not just API
- **Check artifacts:** Mark tasks pending if files don't exist
- **Check type references:** Verify types used in code actually exist (phantom reference detection)
- **Check handoff files:** Verify EVERY file from `prd.implementation.filesToCreate` exists on disk
- **Never skip:** This is the blocker for "missing frontend/test" failures
