import type { EvidenceReadResult } from '../evidence/readEvidence';
import { readEvidenceResult } from '../evidence/readEvidence';
import { captureRepoState } from '../evidence/repoState';
import type { RepoState } from '../evidence/schema';
import {
  extractEvidenceBlockingCauses,
  type EvidenceBlockingCause,
  formatEvidenceBlockingCause,
} from '../evidence/blockingCauses';
import { resolvePolicyForStage } from './stagePolicies';
import { execFileSync } from 'node:child_process';
import { existsSync, realpathSync } from 'node:fs';
import { resolve } from 'node:path';
import type { SkillsLockV1, SkillsStage } from '../config/skillsLock';
import {
  loadEffectiveSkillsLock,
  loadRequiredSkillsLock,
} from '../config/skillsEffectiveLock';
import {
  resolveSkillsEnforcement,
  type SkillsEnforcementResolution,
} from '../policy/skillsEnforcement';
import {
  readMcpAiGateReceipt,
  resolveMcpAiGateReceiptPath,
  type McpAiGateReceiptReadResult,
} from '../mcp/aiGateReceipt';

export type AiGateStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';

export type AiGateViolation = {
  code: string;
  message: string;
  severity: 'ERROR' | 'WARN';
  ruleId?: string;
  file?: string;
  line?: number;
  lines?: readonly number[];
  remediation?: string;
};

type PreWriteWorktreeHygienePolicy = {
  enabled: boolean;
  warnThreshold: number;
  blockThreshold: number;
};

export type AiGateSkillsContractPlatformRequirement = {
  platform: PreWriteSkillsPlatform;
  required_rule_prefix: string;
  required_bundles: ReadonlyArray<string>;
  required_critical_rule_ids: ReadonlyArray<string>;
  required_any_transversal_critical_rule_ids: ReadonlyArray<string>;
  active_prefix_covered: boolean;
  evaluated_prefix_covered: boolean;
  missing_bundles: ReadonlyArray<string>;
  missing_critical_rule_ids: ReadonlyArray<string>;
  transversal_critical_covered: boolean;
  missing_any_transversal_critical_rule_ids: ReadonlyArray<string>;
};

export type AiGateSkillsContractAssessment = {
  stage: AiGateStage;
  enforced: boolean;
  status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
  detected_platforms: ReadonlyArray<PreWriteSkillsPlatform>;
  requirements: ReadonlyArray<AiGateSkillsContractPlatformRequirement>;
  violations: ReadonlyArray<AiGateViolation>;
};

export type AiGateCheckResult = {
  stage: AiGateStage;
  status: 'ALLOWED' | 'BLOCKED';
  allowed: boolean;
  policy: {
    stage: AiGateStage;
    resolved_stage: SkillsStage;
    block_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
    warn_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL';
    trace: {
      source: 'default' | 'skills.policy' | 'hard-mode';
      bundle: string;
      hash: string;
    };
  };
  evidence: {
    kind: EvidenceReadResult['kind'];
    max_age_seconds: number;
    age_seconds: number | null;
    source: {
      source: string;
      path: string;
      digest: string | null;
      generated_at: string | null;
    };
  };
  mcp_receipt: {
    required: boolean;
    kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
    path: string;
    max_age_seconds: number | null;
    age_seconds: number | null;
  };
  skills_contract: AiGateSkillsContractAssessment;
  repo_state: RepoState;
  violations: AiGateViolation[];
};

type AiGateDependencies = {
  now: () => number;
  readEvidenceResult: (repoRoot: string) => EvidenceReadResult;
  readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
  captureRepoState: (repoRoot: string) => RepoState;
  resolvePolicyForStage: (stage: SkillsStage, repoRoot: string) => ReturnType<typeof resolvePolicyForStage>;
  loadEffectiveSkillsLock: (repoRoot: string) => SkillsLockV1 | undefined;
  loadRequiredSkillsLock: (repoRoot: string) => SkillsLockV1 | undefined;
};

const defaultDependencies: AiGateDependencies = {
  now: () => Date.now(),
  readEvidenceResult,
  readMcpAiGateReceipt,
  captureRepoState,
  resolvePolicyForStage,
  loadEffectiveSkillsLock,
  loadRequiredSkillsLock,
};

const DEFAULT_MAX_AGE_SECONDS: Readonly<Record<AiGateStage, number>> = {
  PRE_WRITE: 300,
  PRE_COMMIT: 900,
  PRE_PUSH: 1800,
  CI: 7200,
};
const DEFAULT_PREWRITE_WORKTREE_HYGIENE: PreWriteWorktreeHygienePolicy = {
  enabled: true,
  warnThreshold: 12,
  blockThreshold: 24,
};
const PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV = 'PUMUKI_PREWRITE_WORKTREE_HYGIENE_ENABLED';
const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_WARN_THRESHOLD';
const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD';

const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number];
const PLATFORM_SKILLS_RULE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, string>> = {
  ios: 'skills.ios.',
  android: 'skills.android.',
  backend: 'skills.backend.',
  frontend: 'skills.frontend.',
};
const PLATFORM_REQUIRED_SKILLS_BUNDLES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
  ios: [
    'ios-guidelines',
    'ios-concurrency-guidelines',
    'ios-swiftui-expert-guidelines',
  ],
  android: ['android-guidelines'],
  backend: ['backend-guidelines'],
  frontend: ['frontend-guidelines'],
};
const PREWRITE_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
  ios: ['skills.ios.critical-test-quality'],
  android: [],
  backend: [],
  frontend: [],
};
const PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
  ios: [],
  android: ['skills.android.no-runblocking', 'skills.android.no-thread-sleep'],
  backend: ['skills.backend.no-empty-catch', 'skills.backend.avoid-explicit-any'],
  frontend: ['skills.frontend.no-empty-catch', 'skills.frontend.avoid-explicit-any'],
};
const PREWRITE_PLATFORM_REPO_TREE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
  ios: ['apps/ios/', 'ios/'],
  android: ['apps/android/', 'android/'],
  backend: ['apps/backend/'],
  frontend: ['apps/frontend/', 'apps/web/'],
};
const MCP_RECEIPT_STAGE_ORDER: Readonly<Record<AiGateStage, number>> = {
  PRE_WRITE: 0,
  PRE_COMMIT: 1,
  PRE_PUSH: 2,
  CI: 3,
};
const SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES = new Set([
  'EVIDENCE_MISSING',
  'EVIDENCE_INVALID',
  'EVIDENCE_CHAIN_INVALID',
  'EVIDENCE_TIMESTAMP_INVALID',
  'EVIDENCE_STALE',
]);

const toErrorViolation = (code: string, message: string): AiGateViolation => ({
  code,
  severity: 'ERROR',
  message,
});

const toWarnViolation = (code: string, message: string): AiGateViolation => ({
  code,
  severity: 'WARN',
  message,
});

const normalizeEvidenceCauseLines = (
  lines: EvidenceBlockingCause['lines']
): Pick<AiGateViolation, 'line' | 'lines'> => {
  if (typeof lines === 'number' && Number.isFinite(lines) && lines > 0) {
    return { line: Math.floor(lines) };
  }
  if (Array.isArray(lines)) {
    const normalized = Array.from(
      new Set(
        lines
          .filter((line): line is number => typeof line === 'number' && Number.isFinite(line) && line > 0)
          .map((line) => Math.floor(line))
      )
    ).sort((left, right) => left - right);
    return normalized.length > 0 ? { lines: normalized } : {};
  }
  if (typeof lines === 'string') {
    const normalized = Array.from(
      new Set(
        lines
          .split(',')
          .map((line) => Number.parseInt(line.trim(), 10))
          .filter((line) => Number.isFinite(line) && line > 0)
      )
    ).sort((left, right) => left - right);
    return normalized.length > 0 ? { lines: normalized } : {};
  }
  return {};
};

const toEvidenceGateBlockedViolation = (
  severity: AiGateViolation['severity'],
  message: string,
  cause?: EvidenceBlockingCause
): AiGateViolation => ({
  code: 'EVIDENCE_GATE_BLOCKED',
  severity,
  message,
  ruleId: cause?.ruleId,
  file: cause?.file,
  remediation: cause?.remediation,
  ...normalizeEvidenceCauseLines(cause?.lines),
});

const toSkillsViolation = (
  resolution: SkillsEnforcementResolution,
  code: string,
  message: string
): AiGateViolation => (
  resolution.blocking
    ? toErrorViolation(code, message)
    : toWarnViolation(code, message)
);

const toCriticalSkillsViolation = (code: string, message: string): AiGateViolation =>
  toErrorViolation(code, message);

const buildEvidenceGateBlockedMessage = (result: Extract<EvidenceReadResult, { kind: 'valid' }>): string => {
  const causes = extractEvidenceBlockingCauses(result.evidence);
  if (causes.length === 0) {
    return 'Evidence AI gate status is BLOCKED. No blocking causes were found in evidence; rerun PRE_WRITE audit to regenerate actionable evidence.';
  }
  const formattedCauses = causes
    .slice(0, 8)
    .map((cause, index) => `${index + 1}) ${formatEvidenceBlockingCause(cause)}`);
  const suffix =
    causes.length > formattedCauses.length
      ? ` ... and ${causes.length - formattedCauses.length} more blocking cause(s).`
      : '';
  return `Evidence AI gate status is BLOCKED. Blocking causes: ${formattedCauses.join(' || ')}${suffix}`;
};

const hasEvidenceBlockingCause = (result: Extract<EvidenceReadResult, { kind: 'valid' }>): boolean =>
  extractEvidenceBlockingCauses(result.evidence).length > 0;

const normalizeRepoStateLifecycleVersions = (repoState: RepoState): RepoState => {
  const packageVersion = repoState.lifecycle.package_version;
  const lifecycleVersion = repoState.lifecycle.lifecycle_version;
  if (packageVersion === lifecycleVersion) {
    return repoState;
  }
  const canonicalVersion = packageVersion ?? lifecycleVersion ?? null;
  return {
    ...repoState,
    lifecycle: {
      ...repoState.lifecycle,
      package_version: canonicalVersion,
      lifecycle_version: canonicalVersion,
    },
  };
};

const toPositiveInteger = (value: unknown, fallback: number): number => {
  if (typeof value !== 'number' || !Number.isFinite(value)) {
    return fallback;
  }
  const normalized = Math.trunc(value);
  return normalized > 0 ? normalized : fallback;
};

const toBooleanFromEnv = (value: string | undefined, fallback: boolean): boolean => {
  if (typeof value !== 'string') {
    return fallback;
  }
  const normalized = value.trim().toLowerCase();
  if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
    return true;
  }
  if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
    return false;
  }
  return fallback;
};

const resolvePreWriteWorktreeHygienePolicy = (
  input?: Partial<PreWriteWorktreeHygienePolicy>
): PreWriteWorktreeHygienePolicy => {
  const enabled = input?.enabled
    ?? toBooleanFromEnv(
      process.env[PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV],
      DEFAULT_PREWRITE_WORKTREE_HYGIENE.enabled
    );
  const warnThreshold = toPositiveInteger(
    input?.warnThreshold
      ?? (process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV]
        ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV])
        : undefined),
    DEFAULT_PREWRITE_WORKTREE_HYGIENE.warnThreshold
  );
  const requestedBlockThreshold = toPositiveInteger(
    input?.blockThreshold
      ?? (process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV]
        ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV])
        : undefined),
    DEFAULT_PREWRITE_WORKTREE_HYGIENE.blockThreshold
  );
  const blockThreshold =
    requestedBlockThreshold >= warnThreshold ? requestedBlockThreshold : warnThreshold;

  return {
    enabled,
    warnThreshold,
    blockThreshold,
  };
};

const toTimestampAgeSeconds = (
  timestamp: string,
  nowMs: number
): number | null => {
  const parsed = Date.parse(timestamp);
  if (!Number.isFinite(parsed)) {
    return null;
  }
  const raw = Math.floor((nowMs - parsed) / 1000);
  return raw >= 0 ? raw : 0;
};

const isTimestampFuture = (timestamp: string, nowMs: number): boolean => {
  const parsed = Date.parse(timestamp);
  if (!Number.isFinite(parsed)) {
    return false;
  }
  return parsed > nowMs;
};

const toCanonicalPath = (value: string): string => {
  const normalized = value.replace(/\\/g, '/').toLowerCase();
  try {
    return realpathSync(value).replace(/\\/g, '/').toLowerCase();
  } catch {
    return normalized;
  }
};

const toNormalizedSkillsBundleName = (bundle: string): string => {
  const separatorIndex = bundle.lastIndexOf('@');
  if (separatorIndex <= 0) {
    return bundle.trim();
  }
  return bundle.slice(0, separatorIndex).trim();
};

const toDetectedSkillsPlatforms = (
  platforms: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['platforms'] | undefined
): ReadonlyArray<PreWriteSkillsPlatform> => {
  const platformsState = platforms ?? {};
  const detected: PreWriteSkillsPlatform[] = [];
  for (const platform of PREWRITE_SKILLS_PLATFORMS) {
    if (platformsState[platform]?.detected === true) {
      detected.push(platform);
    }
  }
  return detected;
};

const toCoverageInferredPlatforms = (
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']> | undefined
): ReadonlyArray<PreWriteSkillsPlatform> => {
  if (!coverage) {
    return [];
  }
  const ruleIds = [...coverage.active_rule_ids, ...coverage.evaluated_rule_ids];
  const inferred = new Set<PreWriteSkillsPlatform>();
  for (const ruleId of ruleIds) {
    for (const platform of PREWRITE_SKILLS_PLATFORMS) {
      const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
      if (ruleId.startsWith(prefix)) {
        inferred.add(platform);
      }
    }
  }
  return PREWRITE_SKILLS_PLATFORMS.filter((platform) => inferred.has(platform));
};

const toRepoTreeDetectedPlatforms = (params: {
  repoRoot: string;
  platforms: ReadonlyArray<PreWriteSkillsPlatform>;
}): ReadonlyArray<PreWriteSkillsPlatform> => {
  return params.platforms.filter((platform) => {
    const prefixes = PREWRITE_PLATFORM_REPO_TREE_PREFIXES[platform] ?? [];
    return prefixes.some((prefix) => existsSync(resolve(params.repoRoot, prefix)));
  });
};

const normalizeChangedPath = (value: string): string =>
  value.replace(/\\/g, '/').replace(/^"+|"+$/g, '').trim();

const parseChangedPath = (line: string): string | null => {
  if (line.length < 4) {
    return null;
  }
  const raw = line.slice(3).trim();
  if (raw.length === 0) {
    return null;
  }
  if (raw.includes(' -> ')) {
    const renamed = raw.split(' -> ').pop();
    if (!renamed) {
      return null;
    }
    const normalizedRenamed = normalizeChangedPath(renamed);
    return normalizedRenamed.length > 0 ? normalizedRenamed : null;
  }
  const normalized = normalizeChangedPath(raw);
  return normalized.length > 0 ? normalized : null;
};

const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
  try {
    const output = execFileSync(
      'git',
      ['status', '--short', '--untracked-files=all'],
      {
        cwd: repoRoot,
        encoding: 'utf8',
        stdio: ['ignore', 'pipe', 'ignore'],
      }
    );
    const files = output
      .split('\n')
      .map((line) => parseChangedPath(line))
      .filter((line): line is string => typeof line === 'string' && line.length > 0);
    return [...new Set(files)];
  } catch {
    return [];
  }
};

const collectStagedChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
  try {
    const output = execFileSync(
      'git',
      ['diff', '--cached', '--name-only'],
      {
        cwd: repoRoot,
        encoding: 'utf8',
        stdio: ['ignore', 'pipe', 'ignore'],
      }
    );
    const files = output
      .split('\n')
      .map((line) => normalizeChangedPath(line))
      .filter((line) => line.length > 0);
    return [...new Set(files)];
  } catch {
    return [];
  }
};

const collectPreWriteEffectiveChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
  const stagedPaths = collectStagedChangedPaths(repoRoot);
  if (stagedPaths.length > 0) {
    return stagedPaths;
  }
  return collectWorktreeChangedPaths(repoRoot);
};

const isIosTestQualityPath = (filePath: string): boolean => {
  const normalized = normalizeChangedPath(filePath).toLowerCase();
  if (!normalized.endsWith('.swift')) {
    return false;
  }
  return normalized.includes('/tests/')
    || normalized.includes('/uitests/')
    || normalized.endsWith('test.swift')
    || normalized.endsWith('tests.swift')
    || normalized.endsWith('.spec.swift');
};

const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean =>
  collectPreWriteEffectiveChangedPaths(repoRoot).some((filePath) =>
    isIosTestQualityPath(filePath)
  );

const resolvePreWriteCriticalSkillsRules = (params: {
  platform: PreWriteSkillsPlatform;
  stage: AiGateStage;
  repoRoot: string;
}): ReadonlyArray<string> => {
  const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform];
  if (
    params.platform === 'ios'
    && params.stage === 'PRE_WRITE'
    && ruleIds.includes('skills.ios.critical-test-quality')
    && !hasPreWriteIosTestQualityScope(params.repoRoot)
  ) {
    return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality');
  }
  return ruleIds;
};

const hasRuleCoverage = (
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>,
  ruleId: string
): boolean =>
  coverage.active_rule_ids.includes(ruleId) && coverage.evaluated_rule_ids.includes(ruleId);

const hasEquivalentCriticalRuleCoverage = (
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>,
  ruleId: string
): boolean => {
  if (ruleId !== 'skills.ios.critical-test-quality') {
    return false;
  }
  return hasRuleCoverage(coverage, 'skills.ios.prefer-swift-testing');
};

const hasRequiredCriticalRuleCoverage = (
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>,
  ruleId: string
): boolean =>
  hasRuleCoverage(coverage, ruleId) || hasEquivalentCriticalRuleCoverage(coverage, ruleId);

const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
  const normalized = normalizeChangedPath(filePath).toLowerCase();
  if (platform === 'ios') {
    return normalized.endsWith('.swift')
      || normalized.endsWith('.m')
      || normalized.endsWith('.mm')
      || normalized.endsWith('.h')
      || normalized.endsWith('.xib')
      || normalized.endsWith('.storyboard')
      || normalized.endsWith('.xcstrings')
      || normalized.endsWith('.plist');
  }
  if (platform === 'android') {
    return normalized.endsWith('.kt')
      || normalized.endsWith('.kts')
      || normalized.endsWith('.java')
      || normalized.endsWith('.xml')
      || normalized.endsWith('.gradle')
      || normalized.endsWith('.gradle.kts');
  }
  if (platform === 'backend') {
    const isTypeScriptOrJavaScript =
      normalized.endsWith('.ts')
      || normalized.endsWith('.js')
      || normalized.endsWith('.mts')
      || normalized.endsWith('.cts')
      || normalized.endsWith('.mjs')
      || normalized.endsWith('.cjs');
    if (!isTypeScriptOrJavaScript) {
      return false;
    }
    return normalized.startsWith('apps/backend/')
      || /(^|\/)(backend|server|api)(\/|$)/.test(normalized);
  }
  const isReactExtension = normalized.endsWith('.tsx') || normalized.endsWith('.jsx');
  if (isReactExtension) {
    return true;
  }
  const isTypeScriptOrJavaScript =
    normalized.endsWith('.ts')
    || normalized.endsWith('.js')
    || normalized.endsWith('.mts')
    || normalized.endsWith('.cts')
    || normalized.endsWith('.mjs')
    || normalized.endsWith('.cjs');
  if (!isTypeScriptOrJavaScript) {
    return false;
  }
  return normalized.startsWith('apps/frontend/')
    || normalized.startsWith('apps/web/')
    || /(^|\/)(frontend|web|client)(\/|$)/.test(normalized);
};

const hasWorktreeCodePlatforms = (params: {
  repoRoot: string;
  requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
}): boolean => {
  const changedPaths = collectWorktreeChangedPaths(params.repoRoot);
  if (changedPaths.length === 0) {
    return false;
  }
  return changedPaths.some((filePath) =>
    params.requiredPlatforms.some((platform) => isPlatformPath(platform, filePath))
  );
};

const hasEffectiveChangedCodePlatforms = (params: {
  changedPaths: ReadonlyArray<string>;
  requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
}): boolean =>
  params.changedPaths.some((filePath) =>
    params.requiredPlatforms.some((platform) => isPlatformPath(platform, filePath))
  );

const toChangedPathDetectedPlatforms = (params: {
  changedPaths: ReadonlyArray<string>;
  requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
}): ReadonlyArray<PreWriteSkillsPlatform> => {
  if (params.changedPaths.length === 0 || params.requiredPlatforms.length === 0) {
    return [];
  }
  return params.requiredPlatforms.filter((platform) =>
    params.changedPaths.some((filePath) => isPlatformPath(platform, filePath))
  );
};

const toLockRequiredPlatforms = (
  requiredLock: SkillsLockV1 | undefined
): ReadonlyArray<PreWriteSkillsPlatform> => {
  if (!requiredLock) {
    return [];
  }

  const requiredBundles = new Set(
    requiredLock.bundles.map((bundle) => toNormalizedSkillsBundleName(bundle.name))
  );
  const required = new Set<PreWriteSkillsPlatform>();

  for (const platform of PREWRITE_SKILLS_PLATFORMS) {
    const requiredByBundle = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform].some((bundleName) =>
      requiredBundles.has(bundleName)
    );
    const requiredByRules = requiredLock.bundles.some((bundle) =>
      bundle.rules.some(
        (rule) => PREWRITE_SKILLS_PLATFORMS.includes(rule.platform as PreWriteSkillsPlatform)
          && rule.platform === platform
      )
    );

    if (requiredByBundle || requiredByRules) {
      required.add(platform);
    }
  }

  return PREWRITE_SKILLS_PLATFORMS.filter((platform) => required.has(platform));
};

const toEffectiveSkillsPlatforms = (params: {
  platforms: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['platforms'] | undefined;
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']> | undefined;
}): ReadonlyArray<PreWriteSkillsPlatform> => {
  const detectedPlatforms = toDetectedSkillsPlatforms(params.platforms);
  const inferredFromCoverage = toCoverageInferredPlatforms(params.coverage);
  if (detectedPlatforms.length === 0) {
    return inferredFromCoverage;
  }
  if (inferredFromCoverage.length === 0) {
    return detectedPlatforms;
  }
  const inferredSet = new Set(inferredFromCoverage);
  const intersection = detectedPlatforms.filter((platform) => inferredSet.has(platform));
  if (intersection.length > 0) {
    return intersection;
  }
  return detectedPlatforms;
};

const toPreWriteDetectedSkillsPlatforms = (params: {
  platforms: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['platforms'] | undefined;
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']> | undefined;
}): ReadonlyArray<PreWriteSkillsPlatform> => {
  const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.platforms);
  if (explicitlyDetectedPlatforms.length === 0) {
    return [];
  }
  return toEffectiveSkillsPlatforms(params);
};

const collectActiveRuleIdsCoverageViolations = (params: {
  stage: AiGateStage;
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
}): AiGateViolation[] => {
  if (params.coverage.active_rule_ids.length > 0) {
    return [];
  }
  const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
  const effectivePlatforms = toEffectiveSkillsPlatforms({
    platforms: params.evidence.platforms,
    coverage: params.coverage,
  });
  const inferredPlatforms = toCoverageInferredPlatforms(params.coverage);
  const blockedPlatforms = effectivePlatforms.length > 0
    ? effectivePlatforms
    : inferredPlatforms;
  if (blockedPlatforms.length === 0) {
    return [];
  }
  const detectionMode = explicitlyDetectedPlatforms.length > 0 ? 'detected' : 'inferred';
  return [
    toErrorViolation(
      'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES',
      `Active rules coverage is empty at ${params.stage} with ${detectionMode} code platforms=[${blockedPlatforms.join(', ')}].`
    ),
  ];
};

const collectPreWritePlatformSkillsViolations = (params: {
  repoRoot: string;
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
  skillsEnforcement: SkillsEnforcementResolution;
}): AiGateViolation[] => {
  const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
    platforms: params.evidence.platforms,
    coverage: params.coverage,
  });
  if (detectedPlatforms.length === 0) {
    return [];
  }
  const effectiveChangedPaths = collectPreWriteEffectiveChangedPaths(params.repoRoot);
  const isNoCodeEffectiveScope =
    effectiveChangedPaths.length > 0
    && !hasEffectiveChangedCodePlatforms({
      changedPaths: effectiveChangedPaths,
      requiredPlatforms: detectedPlatforms,
    });
  if (isNoCodeEffectiveScope) {
    return [];
  }

  const violations: AiGateViolation[] = [];
  const missingScopeCoverage: string[] = [];
  const missingBundlesByPlatform: string[] = [];

  for (const platform of detectedPlatforms) {
    const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
    const hasActivePrefix = params.coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(prefix));
    const hasEvaluatedPrefix = params.coverage.evaluated_rule_ids.some((ruleId) =>
      ruleId.startsWith(prefix)
    );

    if (!hasActivePrefix || !hasEvaluatedPrefix) {
      const reasons: string[] = [];
      if (!hasActivePrefix) {
        reasons.push(`active_rules_prefix=${prefix} missing`);
      }
      if (!hasEvaluatedPrefix) {
        reasons.push(`evaluated_rules_prefix=${prefix} missing`);
      }
      missingScopeCoverage.push(`${platform}{${reasons.join('; ')}}`);
    }
  }

  if (missingScopeCoverage.length > 0) {
    violations.push(
      toSkillsViolation(
        params.skillsEnforcement,
        'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
        `Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.`
      )
    );
  }

  const activeSkillsBundles = new Set(
    (params.evidence.rulesets ?? [])
      .filter((ruleset) => ruleset.platform === 'skills')
      .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
      .filter((bundle) => bundle.length > 0)
  );

  for (const platform of detectedPlatforms) {
    const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform];
    const missingBundles = requiredBundles.filter((bundleName) => !activeSkillsBundles.has(bundleName));
    if (missingBundles.length === 0) {
      continue;
    }
    missingBundlesByPlatform.push(`${platform}{missing_bundles=[${missingBundles.join(', ')}]}`);
  }

  if (missingBundlesByPlatform.length > 0) {
    violations.push(
      toSkillsViolation(
        params.skillsEnforcement,
        'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
        `Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.`
      )
    );
  }

  const missingCriticalRulesByPlatform: string[] = [];
  for (const platform of detectedPlatforms) {
    const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({
      platform,
      stage: 'PRE_WRITE',
      repoRoot: params.repoRoot,
    });
    if (requiredCriticalRuleIds.length === 0) {
      continue;
    }
    const missingCriticalRuleIds = requiredCriticalRuleIds.filter((ruleId) => {
      return !hasRequiredCriticalRuleCoverage(params.coverage, ruleId);
    });
    if (missingCriticalRuleIds.length === 0) {
      continue;
    }
    missingCriticalRulesByPlatform.push(
      `${platform}{missing_critical_rule_ids=[${missingCriticalRuleIds.join(', ')}]}`
    );
  }

  if (missingCriticalRulesByPlatform.length > 0) {
    violations.push(
      toSkillsViolation(
        params.skillsEnforcement,
        'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
        `Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.`
      )
    );
  }

  return violations;
};

const collectPreWriteCrossPlatformCriticalViolations = (params: {
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
  skillsEnforcement: SkillsEnforcementResolution;
}): AiGateViolation[] => {
  const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({
    platforms: params.evidence.platforms,
    coverage: params.coverage,
  });
  if (detectedPlatforms.length === 0) {
    return [];
  }

  const missingCriticalCoverage: string[] = [];
  for (const platform of detectedPlatforms) {
    const requiredRuleIds = PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform];
    if (requiredRuleIds.length === 0) {
      continue;
    }

    const hasCoverage = requiredRuleIds.some((ruleId) =>
      params.coverage.active_rule_ids.includes(ruleId) &&
      params.coverage.evaluated_rule_ids.includes(ruleId)
    );

    if (hasCoverage) {
      continue;
    }

    missingCriticalCoverage.push(
      `${platform}{required_any=[${requiredRuleIds.join(', ')}]}`
    );
  }

  if (missingCriticalCoverage.length === 0) {
    return [];
  }

  return [];
};

const toSkillsContractAssessment = (params: {
  stage: AiGateStage;
  repoRoot: string;
  repoState: RepoState;
  evidenceResult: EvidenceReadResult;
  requiredLock?: SkillsLockV1;
  skillsEnforcement: SkillsEnforcementResolution;
}): AiGateSkillsContractAssessment => {
  const requiredPlatforms = toLockRequiredPlatforms(params.requiredLock);
  const effectiveChangedPaths = collectPreWriteEffectiveChangedPaths(params.repoRoot);
  const hasEffectiveChangedPaths = effectiveChangedPaths.length > 0;
  const hasEffectiveCodePlatforms = hasEffectiveChangedCodePlatforms({
    changedPaths: effectiveChangedPaths,
    requiredPlatforms,
  });
  const isNoCodeEffectiveScope =
    requiredPlatforms.length > 0 && hasEffectiveChangedPaths && !hasEffectiveCodePlatforms;

  if (isNoCodeEffectiveScope) {
    return {
      stage: params.stage,
      enforced: false,
      status: 'NOT_APPLICABLE',
      detected_platforms: [],
      requirements: [],
      violations: [],
    };
  }

  if (params.evidenceResult.kind !== 'valid') {
    return {
      stage: params.stage,
      enforced: requiredPlatforms.length > 0,
      status: requiredPlatforms.length > 0 ? 'FAIL' : 'NOT_APPLICABLE',
      detected_platforms: [],
      requirements: requiredPlatforms.map((platform) => ({
        platform,
        required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
        required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
        required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
          platform,
          stage: params.stage,
          repoRoot: params.repoRoot,
        }),
        required_any_transversal_critical_rule_ids: [
          ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
        ],
        active_prefix_covered: false,
        evaluated_prefix_covered: false,
        missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
        missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
          platform,
          stage: params.stage,
          repoRoot: params.repoRoot,
        }),
        transversal_critical_covered: false,
        missing_any_transversal_critical_rule_ids: [
          ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
        ],
      })),
      violations:
        requiredPlatforms.length > 0
          ? [
              toSkillsViolation(
                params.skillsEnforcement,
                'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
                `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
              ),
            ]
          : [],
    };
  }

  const coverage = params.evidenceResult.evidence.snapshot.rules_coverage;
  const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
  const inferredPlatforms = toCoverageInferredPlatforms(coverage);
  const changedPathDetectedPlatforms = toChangedPathDetectedPlatforms({
    changedPaths: effectiveChangedPaths,
    requiredPlatforms,
  });
  const repoTreeDetectedPlatforms =
    params.stage !== 'PRE_WRITE' && requiredPlatforms.length > 0 && !hasEffectiveChangedPaths
      ? toRepoTreeDetectedPlatforms({
          repoRoot: params.repoRoot,
          platforms: requiredPlatforms,
        })
      : [];
  const explicitlyDetectedEffectivePlatforms =
    explicitlyDetectedPlatforms.length > 0
      ? toEffectiveSkillsPlatforms({
          platforms: params.evidenceResult.evidence.platforms,
          coverage,
        })
      : [];
  const detectedPlatforms =
    explicitlyDetectedEffectivePlatforms.length > 0
      ? explicitlyDetectedEffectivePlatforms
      : inferredPlatforms.length > 0
        ? inferredPlatforms
        : changedPathDetectedPlatforms.length > 0
          ? changedPathDetectedPlatforms
          : repoTreeDetectedPlatforms;
  const pendingChanges = resolvePendingChanges(params.repoState);
  const detectedPlatformSet = new Set(detectedPlatforms);
  const assessmentPlatforms =
    requiredPlatforms.length > 0
      ? params.stage === 'PRE_WRITE' && detectedPlatforms.length > 0
        ? requiredPlatforms.filter((platform) => detectedPlatformSet.has(platform))
        : requiredPlatforms
      : detectedPlatforms;

  if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
    if (
      (
        pendingChanges === 0
        || !hasEffectiveCodePlatforms
        || (
          params.stage === 'PRE_WRITE'
          && !hasWorktreeCodePlatforms({
            repoRoot: params.repoRoot,
            requiredPlatforms,
          })
        )
      )
    ) {
      return {
        stage: params.stage,
        enforced: false,
        status: 'NOT_APPLICABLE',
        detected_platforms: [],
        requirements: [],
        violations: [],
      };
    }
    const requirements: AiGateSkillsContractPlatformRequirement[] = requiredPlatforms.map((platform) => ({
      platform,
      required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
      required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
      required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
        platform,
        stage: params.stage,
        repoRoot: params.repoRoot,
      }),
      required_any_transversal_critical_rule_ids: [
        ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
      ],
      active_prefix_covered: false,
      evaluated_prefix_covered: false,
      missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
      missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
        platform,
        stage: params.stage,
        repoRoot: params.repoRoot,
      }),
      transversal_critical_covered: false,
      missing_any_transversal_critical_rule_ids: [
        ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
      ],
    }));

    return {
      stage: params.stage,
      enforced: true,
      status: 'FAIL',
      detected_platforms: [],
      requirements,
      violations: [
        toSkillsViolation(
          params.skillsEnforcement,
          'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
          `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
        ),
      ],
    };
  }
  if (assessmentPlatforms.length === 0) {
    return {
      stage: params.stage,
      enforced: false,
      status: 'NOT_APPLICABLE',
      detected_platforms: [],
      requirements: [],
      violations: [],
    };
  }

  const activeSkillsBundles = new Set(
    (params.evidenceResult.evidence.rulesets ?? [])
      .filter((ruleset) => ruleset.platform === 'skills')
      .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
      .filter((bundle) => bundle.length > 0)
  );

  const requirements: AiGateSkillsContractPlatformRequirement[] = [];
  const violations: AiGateViolation[] = [];
  if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) {
    violations.push(
      toSkillsViolation(
        params.skillsEnforcement,
        'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED',
        `Required repo skills exist, but active platforms could not be detected for ${params.stage}.`
      )
    );
  }
  for (const platform of assessmentPlatforms) {
    const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
    const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
    const requiredCriticalRuleIds = [
      ...resolvePreWriteCriticalSkillsRules({
        platform,
        stage: params.stage,
        repoRoot: params.repoRoot,
      }),
    ];
    const requiredAnyTransversalCriticalRuleIds = [
      ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
    ];
    const activePrefixCovered = coverage
      ? coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
      : false;
    const evaluatedPrefixCovered = coverage
      ? coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
      : false;
    const missingBundles = requiredBundles.filter(
      (bundleName) => !activeSkillsBundles.has(bundleName)
    );
    const missingCriticalRuleIds = coverage
      ? requiredCriticalRuleIds.filter((ruleId) => {
          return !hasRequiredCriticalRuleCoverage(coverage, ruleId);
        })
      : [...requiredCriticalRuleIds];
    const transversalCriticalCovered =
      requiredAnyTransversalCriticalRuleIds.length === 0
        ? true
        : coverage
          ? requiredAnyTransversalCriticalRuleIds.some((ruleId) =>
              coverage.active_rule_ids.includes(ruleId)
              && coverage.evaluated_rule_ids.includes(ruleId)
            )
          : false;
    const missingAnyTransversalCriticalRuleIds = transversalCriticalCovered
      ? []
      : [...requiredAnyTransversalCriticalRuleIds];

    requirements.push({
      platform,
      required_rule_prefix: requiredRulePrefix,
      required_bundles: requiredBundles,
      required_critical_rule_ids: requiredCriticalRuleIds,
      required_any_transversal_critical_rule_ids: requiredAnyTransversalCriticalRuleIds,
      active_prefix_covered: activePrefixCovered,
      evaluated_prefix_covered: evaluatedPrefixCovered,
      missing_bundles: missingBundles,
      missing_critical_rule_ids: missingCriticalRuleIds,
      transversal_critical_covered: transversalCriticalCovered,
      missing_any_transversal_critical_rule_ids: missingAnyTransversalCriticalRuleIds,
    });

    if (!activePrefixCovered || !evaluatedPrefixCovered) {
      const missingParts: string[] = [];
      if (!activePrefixCovered) {
        missingParts.push('active_prefix');
      }
      if (!evaluatedPrefixCovered) {
        missingParts.push('evaluated_prefix');
      }
      violations.push(
        toSkillsViolation(
          params.skillsEnforcement,
          'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
          `Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).`
        )
      );
    }
    if (missingBundles.length > 0) {
      violations.push(
        toSkillsViolation(
          params.skillsEnforcement,
          'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
          `Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`
        )
      );
    }
    if (missingCriticalRuleIds.length > 0) {
      violations.push(
        toCriticalSkillsViolation(
          'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
          `Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`
        )
      );
    }
  }

  return {
    stage: params.stage,
    enforced: true,
    status: violations.length === 0 ? 'PASS' : 'FAIL',
    detected_platforms: detectedPlatforms,
    requirements,
    violations,
  };
};

const collectPreWriteCoherenceViolations = (params: {
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
  repoRoot: string;
  repoState: RepoState;
  nowMs: number;
  preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy;
}): AiGateViolation[] => {
  const violations: AiGateViolation[] = [];
  const evidenceRepoRoot = params.evidence.repo_state?.repo_root;
  if (
    typeof evidenceRepoRoot === 'string' &&
    evidenceRepoRoot.trim().length > 0 &&
    toCanonicalPath(evidenceRepoRoot) !== toCanonicalPath(params.repoRoot)
  ) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_REPO_ROOT_MISMATCH',
        `Evidence repo root mismatch (${evidenceRepoRoot} != ${params.repoRoot}).`
      )
    );
  }

  const evidenceBranch = params.evidence.repo_state?.git?.branch;
  const currentBranch = params.repoState.git.branch;
  if (
    typeof evidenceBranch === 'string' &&
    evidenceBranch.trim().length > 0 &&
    typeof currentBranch === 'string' &&
    currentBranch.trim().length > 0 &&
    evidenceBranch !== currentBranch
  ) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_BRANCH_MISMATCH',
        `Evidence branch mismatch (${evidenceBranch} != ${currentBranch}).`
      )
    );
  }

  if (params.evidence.severity_metrics.gate_status !== params.evidence.ai_gate.status) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_GATE_STATUS_INCOHERENT',
        `Evidence gate status mismatch (severity_metrics=${params.evidence.severity_metrics.gate_status} ai_gate=${params.evidence.ai_gate.status}).`
      )
    );
  }

  if (params.evidence.ai_gate.status === 'BLOCKED' && params.evidence.snapshot.outcome !== 'BLOCK') {
    violations.push(
      toErrorViolation(
        'EVIDENCE_OUTCOME_INCOHERENT',
        `Evidence outcome mismatch (ai_gate=BLOCKED snapshot.outcome=${params.evidence.snapshot.outcome}).`
      )
    );
  }

  const coverage = params.evidence.snapshot.rules_coverage;
  if (!coverage) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_RULES_COVERAGE_MISSING',
        'Evidence rules_coverage is missing for PRE_WRITE validation.'
      )
    );
  } else {
    const skillsEnforcement = resolveSkillsEnforcement();
    if (coverage.stage !== params.evidence.snapshot.stage) {
      violations.push(
        toErrorViolation(
          'EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH',
          `rules_coverage stage mismatch (${coverage.stage} != ${params.evidence.snapshot.stage}).`
        )
      );
    }
    if (coverage.counts.unevaluated > 0 || coverage.coverage_ratio < 1) {
      violations.push(
        toErrorViolation(
          'EVIDENCE_RULES_COVERAGE_INCOMPLETE',
          `rules_coverage incomplete (unevaluated=${coverage.counts.unevaluated}, ratio=${coverage.coverage_ratio}).`
        )
      );
    }
    const unsupportedAutoCount =
      coverage.counts.unsupported_auto
      ?? coverage.unsupported_auto_rule_ids?.length
      ?? 0;
    if (unsupportedAutoCount > 0) {
      violations.push(
        toErrorViolation(
          'EVIDENCE_UNSUPPORTED_AUTO_RULES',
          `rules_coverage has unsupported auto rules (${unsupportedAutoCount}).`
        )
      );
    }

    violations.push(
      ...collectActiveRuleIdsCoverageViolations({
        stage: 'PRE_WRITE',
        evidence: params.evidence,
        coverage,
      })
    );

    violations.push(
      ...collectPreWritePlatformSkillsViolations({
        repoRoot: params.repoRoot,
        evidence: params.evidence,
        coverage,
        skillsEnforcement,
      })
    );
    violations.push(
      ...collectPreWriteCrossPlatformCriticalViolations({
        evidence: params.evidence,
        coverage,
        skillsEnforcement,
      })
    );
  }

  if (isTimestampFuture(params.evidence.timestamp, params.nowMs)) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_TIMESTAMP_FUTURE',
        'Evidence timestamp is in the future.'
      )
    );
  }

  return violations;
};

const collectEvidenceViolations = (
  result: EvidenceReadResult,
  repoRoot: string,
  repoState: RepoState,
  stage: AiGateStage,
  nowMs: number,
  maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>,
  preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy
): { violations: AiGateViolation[]; ageSeconds: number | null } => {
  const violations: AiGateViolation[] = [];
  const maxAgeSeconds = maxAgeSecondsByStage[stage];

  if (result.kind === 'missing') {
    violations.push(toErrorViolation('EVIDENCE_MISSING', '.ai_evidence.json is missing.'));
    return { violations, ageSeconds: null };
  }

  if (result.kind === 'invalid') {
    const invalidCode =
      result.reason === 'evidence-chain-invalid'
        ? 'EVIDENCE_CHAIN_INVALID'
        : 'EVIDENCE_INVALID';
    const detailSuffix = result.detail ? ` ${result.detail}` : '';
    violations.push(
      toErrorViolation(
        invalidCode,
        `.ai_evidence.json is invalid${result.version ? ` (version=${result.version})` : ''}.${detailSuffix}`
      )
    );
    return { violations, ageSeconds: null };
  }

  const ageSeconds = toTimestampAgeSeconds(result.evidence.timestamp, nowMs);
  if (ageSeconds === null) {
    violations.push(toErrorViolation('EVIDENCE_TIMESTAMP_INVALID', 'Evidence timestamp is invalid.'));
    return { violations, ageSeconds: null };
  }

  if (ageSeconds > maxAgeSeconds) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_STALE',
        `Evidence is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${stage}).`
      )
    );
  }

  if (result.evidence.ai_gate.status === 'BLOCKED') {
    const blockingCause = extractEvidenceBlockingCauses(result.evidence)[0];
    const gateBlockedMessage = buildEvidenceGateBlockedMessage(result);
    violations.push(
      !hasEvidenceBlockingCause(result) && ageSeconds <= maxAgeSeconds
        ? toWarnViolation(
            'EVIDENCE_GATE_BLOCKED',
            `${gateBlockedMessage} Advisory because the evidence snapshot did not expose any blocking cause.`
          )
        : isAdvisoryDocumentationRenderSlice(repoRoot, ageSeconds, maxAgeSeconds)
        ? toWarnViolation(
            'EVIDENCE_GATE_BLOCKED',
            `${gateBlockedMessage} Advisory because the current slice only contains documentation/render/tooling artifacts and evidence is fresh.`
          )
        : toEvidenceGateBlockedViolation('ERROR', gateBlockedMessage, blockingCause)
    );
  }

  if (stage === 'PRE_WRITE') {
    violations.push(
      ...collectPreWriteCoherenceViolations({
        evidence: result.evidence,
        repoRoot,
        repoState,
        nowMs,
        preWriteWorktreeHygiene,
      })
    );
  }

  appendWorktreeHygieneViolations(violations, repoState, preWriteWorktreeHygiene, stage);

  return { violations, ageSeconds };
};

const SUPPORTED_FUNCTIONAL_EXTENSIONS = new Set([
  '.swift',
  '.ts',
  '.tsx',
  '.js',
  '.jsx',
  '.kt',
  '.kts',
]);

const DOCUMENTATION_RENDER_TOOLING_EXTENSIONS = new Set([
  '.css',
  '.html',
  '.json',
  '.md',
  '.mdx',
  '.txt',
]);

const normalizeGitPath = (value: string): string => value.replace(/\\/g, '/').replace(/^"|"$/g, '');

const extractGitStatusPath = (line: string): string | null => {
  const payload = line.slice(3).trim();
  if (payload.length === 0) {
    return null;
  }
  const renameSeparator = ' -> ';
  if (payload.includes(renameSeparator)) {
    return normalizeGitPath(payload.slice(payload.lastIndexOf(renameSeparator) + renameSeparator.length));
  }
  return normalizeGitPath(payload);
};

const collectGitStatusPaths = (repoRoot: string): readonly string[] => {
  if (!existsSync(resolve(repoRoot, '.git'))) {
    return [];
  }
  try {
    return execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
      cwd: repoRoot,
      encoding: 'utf8',
      stdio: ['ignore', 'pipe', 'ignore'],
    })
      .split(/\r?\n/)
      .map(extractGitStatusPath)
      .filter((path): path is string => path !== null);
  } catch {
    return [];
  }
};

const pathExtension = (path: string): string => {
  const basename = path.split('/').pop() ?? '';
  const dotIndex = basename.lastIndexOf('.');
  return dotIndex >= 0 ? basename.slice(dotIndex).toLowerCase() : '';
};

const isSupportedFunctionalPath = (path: string): boolean =>
  SUPPORTED_FUNCTIONAL_EXTENSIONS.has(pathExtension(path));

const isDocumentationRenderToolingPath = (path: string): boolean => {
  const normalized = path.toLowerCase();
  if (normalized.startsWith('docs/') && DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized))) {
    return true;
  }
  if (normalized.endsWith('.md') || normalized.endsWith('.mdx')) {
    return true;
  }
  if (
    normalized.startsWith('stack-my-architecture-pumuki/dist/') ||
    normalized.startsWith('stack-my-architecture-hub/pumuki/')
  ) {
    return DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized));
  }
  if (normalized === 'stack-my-architecture-pumuki/scripts/build-html.py') {
    return true;
  }
  if (normalized === 'package.json' || normalized === 'package-lock.json') {
    return true;
  }
  return false;
};

const isAdvisoryDocumentationRenderSlice = (
  repoRoot: string,
  ageSeconds: number,
  maxAgeSeconds: number
): boolean => {
  if (ageSeconds > maxAgeSeconds) {
    return false;
  }
  const paths = collectGitStatusPaths(repoRoot);
  if (paths.length === 0) {
    return false;
  }
  return (
    paths.every(isDocumentationRenderToolingPath) &&
    !paths.some(isSupportedFunctionalPath)
  );
};

const toEvidenceSourceDescriptor = (
  result: EvidenceReadResult,
  repoRoot: string
): AiGateCheckResult['evidence']['source'] => {
  if ('source_descriptor' in result) {
    return {
      source: result.source_descriptor.source,
      path: result.source_descriptor.path,
      digest: result.source_descriptor.digest,
      generated_at: result.source_descriptor.generated_at,
    };
  }

  return {
    source: 'local-file',
    path: resolve(repoRoot, '.ai_evidence.json'),
    digest: null,
    generated_at: null,
  };
};

const collectGitflowViolations = (
  repoState: RepoState,
  protectedBranches: ReadonlySet<string>
): AiGateViolation[] => {
  const violations: AiGateViolation[] = [];
  if (!repoState.git.available) {
    return violations;
  }
  if (repoState.git.branch && protectedBranches.has(repoState.git.branch)) {
    violations.push(
      toErrorViolation(
        'GITFLOW_PROTECTED_BRANCH',
        `Direct work on protected branch "${repoState.git.branch}" is not allowed.`
      )
    );
  }
  return violations;
};

const resolvePendingChanges = (repoState: RepoState): number | null => {
  if (!repoState.git.available) {
    return null;
  }
  return repoState.git.pending_changes ?? (repoState.git.staged + repoState.git.unstaged);
};

const appendWorktreeHygieneViolations = (
  violations: AiGateViolation[],
  repoState: RepoState,
  policy: PreWriteWorktreeHygienePolicy,
  stageLabel: AiGateStage
): void => {
  if (!policy.enabled || !repoState.git.available) {
    return;
  }
  const pendingChanges = resolvePendingChanges(repoState) ?? 0;
  if (pendingChanges >= policy.blockThreshold) {
    violations.push(
      toErrorViolation(
        'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
        `${stageLabel} worktree hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${policy.blockThreshold}). Split worktree into atomic slices.`
      )
    );
  } else if (pendingChanges >= policy.warnThreshold) {
    violations.push(
      toWarnViolation(
        'EVIDENCE_PREWRITE_WORKTREE_WARN',
        `${stageLabel} worktree hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${policy.warnThreshold}). Consider smaller staged/unstaged batches.`
      )
    );
  }
};

const toPolicyStage = (stage: AiGateStage): SkillsStage => {
  return stage;
};

const isMcpReceiptStageCompatible = (params: {
  receiptStage: AiGateStage;
  requestedStage: AiGateStage;
}): boolean => {
  return MCP_RECEIPT_STAGE_ORDER[params.receiptStage] >= MCP_RECEIPT_STAGE_ORDER[params.requestedStage];
};

const collectMcpReceiptViolations = (params: {
  required: boolean;
  stage: AiGateStage;
  repoRoot: string;
  nowMs: number;
  maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>;
  readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult;
}): {
  required: boolean;
  kind: 'disabled' | McpAiGateReceiptReadResult['kind'];
  path: string;
  maxAgeSeconds: number | null;
  ageSeconds: number | null;
  violations: AiGateViolation[];
} => {
  const path = resolveMcpAiGateReceiptPath(params.repoRoot);
  if (!params.required || params.stage !== 'PRE_WRITE') {
    return {
      required: params.required,
      kind: 'disabled',
      path,
      maxAgeSeconds: null,
      ageSeconds: null,
      violations: [],
    };
  }

  const maxAgeSeconds = params.maxAgeSecondsByStage[params.stage];
  const receiptRead = params.readMcpAiGateReceipt(params.repoRoot);
  if (receiptRead.kind === 'missing') {
    return {
      required: true,
      kind: 'missing',
      path: receiptRead.path,
      maxAgeSeconds,
      ageSeconds: null,
      violations: [
        toErrorViolation(
          'MCP_ENTERPRISE_RECEIPT_MISSING',
          'MCP receipt is missing. Call tool ai_gate_check in pumuki-enterprise MCP before PRE_WRITE.'
        ),
      ],
    };
  }

  if (receiptRead.kind === 'invalid') {
    return {
      required: true,
      kind: 'invalid',
      path: receiptRead.path,
      maxAgeSeconds,
      ageSeconds: null,
      violations: [
        toErrorViolation(
          'MCP_ENTERPRISE_RECEIPT_INVALID',
          `MCP receipt is invalid: ${receiptRead.reason}`
        ),
      ],
    };
  }

  const violations: AiGateViolation[] = [];
  if (toCanonicalPath(receiptRead.receipt.repo_root) !== toCanonicalPath(params.repoRoot)) {
    violations.push(
      toErrorViolation(
        'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH',
        `MCP receipt repo root mismatch (${receiptRead.receipt.repo_root} != ${params.repoRoot}).`
      )
    );
  }
  if (
    !isMcpReceiptStageCompatible({
      receiptStage: receiptRead.receipt.stage,
      requestedStage: params.stage,
    })
  ) {
    violations.push(
      toErrorViolation(
        'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH',
        `MCP receipt stage mismatch (${receiptRead.receipt.stage} incompatible with ${params.stage}).`
      )
    );
  }
  const ageSeconds = toTimestampAgeSeconds(receiptRead.receipt.issued_at, params.nowMs);
  if (ageSeconds === null) {
    violations.push(
      toErrorViolation(
        'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID',
        'MCP receipt issued_at timestamp is invalid.'
      )
    );
  } else if (ageSeconds > maxAgeSeconds) {
    violations.push(
      toErrorViolation(
        'MCP_ENTERPRISE_RECEIPT_STALE',
        `MCP receipt is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${params.stage}).`
      )
    );
  }
  if (isTimestampFuture(receiptRead.receipt.issued_at, params.nowMs)) {
    violations.push(
      toErrorViolation(
        'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE',
        'MCP receipt timestamp is in the future.'
      )
    );
  }

  return {
    required: true,
    kind: 'valid',
    path: receiptRead.path,
    maxAgeSeconds,
    ageSeconds,
    violations,
  };
};

export const evaluateAiGate = (
  params: {
    repoRoot: string;
    stage: AiGateStage;
    maxAgeSecondsByStage?: Readonly<Record<AiGateStage, number>>;
    protectedBranches?: ReadonlyArray<string>;
    requireMcpReceipt?: boolean;
    preWriteWorktreeHygiene?: Partial<PreWriteWorktreeHygienePolicy>;
  },
  dependencies: Partial<AiGateDependencies> = {}
): AiGateCheckResult => {
  const activeDependencies: AiGateDependencies = {
    ...defaultDependencies,
    ...dependencies,
  };
  const maxAgeSecondsByStage = params.maxAgeSecondsByStage ?? DEFAULT_MAX_AGE_SECONDS;
  const preWriteWorktreeHygiene = resolvePreWriteWorktreeHygienePolicy(
    params.preWriteWorktreeHygiene
  );
  const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES));
  const nowMs = activeDependencies.now();
  const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot);
  const repoState = normalizeRepoStateLifecycleVersions(
    activeDependencies.captureRepoState(params.repoRoot)
  );
  const effectiveSkillsLock = activeDependencies.loadEffectiveSkillsLock(params.repoRoot);
  const requiredSkillsLock = activeDependencies.loadRequiredSkillsLock(params.repoRoot);
  const requiredSkillsPlatforms = toLockRequiredPlatforms(requiredSkillsLock);
  const policyStage = toPolicyStage(params.stage);
  const resolvedPolicy = activeDependencies.resolvePolicyForStage(
    policyStage,
    params.repoRoot
  );
  const skillsEnforcement = resolveSkillsEnforcement();
  const evidenceAssessment = collectEvidenceViolations(
    evidenceResult,
    params.repoRoot,
    repoState,
    params.stage,
    nowMs,
    maxAgeSecondsByStage,
    preWriteWorktreeHygiene
  );
  const mcpReceiptAssessment = collectMcpReceiptViolations({
    required: params.requireMcpReceipt ?? false,
    stage: params.stage,
    repoRoot: params.repoRoot,
    nowMs,
    maxAgeSecondsByStage,
    readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
  });
  const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
  const skillsContract = toSkillsContractAssessment({
    stage: params.stage,
    repoRoot: params.repoRoot,
    repoState,
    evidenceResult,
    requiredLock: requiredSkillsLock,
    skillsEnforcement,
  });
  const suppressSkillsContractViolation = evidenceAssessment.violations.some((violation) =>
    SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES.has(violation.code)
  );
  const stageSkillsContractViolations =
    suppressSkillsContractViolation
    || skillsContract.status !== 'FAIL'
    || (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0)
    || skillsContract.violations.some((violation) =>
      violation.code === 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING'
    )
        ? []
      : [
          toSkillsViolation(
            skillsEnforcement,
            'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
            `Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
          ),
        ];
  const violations = [
    ...evidenceAssessment.violations,
    ...stageSkillsContractViolations,
    ...gitflowViolations,
    ...mcpReceiptAssessment.violations,
  ];
  const blocked = violations.some((violation) => violation.severity === 'ERROR');

  return {
    stage: params.stage,
    status: blocked ? 'BLOCKED' : 'ALLOWED',
    allowed: !blocked,
    policy: {
      stage: params.stage,
      resolved_stage: policyStage,
      block_on_or_above: resolvedPolicy.policy.blockOnOrAbove,
      warn_on_or_above: resolvedPolicy.policy.warnOnOrAbove,
      trace: resolvedPolicy.trace,
    },
    evidence: {
      kind: evidenceResult.kind,
      max_age_seconds: maxAgeSecondsByStage[params.stage],
      age_seconds: evidenceAssessment.ageSeconds,
      source: toEvidenceSourceDescriptor(evidenceResult, params.repoRoot),
    },
    mcp_receipt: {
      required: mcpReceiptAssessment.required,
      kind: mcpReceiptAssessment.kind,
      path: mcpReceiptAssessment.path,
      max_age_seconds: mcpReceiptAssessment.maxAgeSeconds,
      age_seconds: mcpReceiptAssessment.ageSeconds,
    },
    skills_contract: skillsContract,
    repo_state: repoState,
    violations,
  };
};
