# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Presubmit script for multi-agent-engineering-workflow.

This script enforces structural integrity and formatting for the workflow
protocol markdown documentation and persona cheat sheets.
"""

import collections
import importlib.util
import json
import os
import re

# Regex for tokens exempt from the 80-character line limit.
# Group 1: Absolute src/... paths
# Group 2: Standard URLs (excludes trailing punctuation common in markdown)
# Group 3: Relative markdown links (must not start with src/ or http)
EXEMPT_TOKENS_RE = re.compile(
    r'(src/[a-zA-Z0-9_/\.\-]+)|'
    r'(https?://[^\s()<>\[\]]+[^\s()<>\[\].:;?])|'
    r'\[[^\]]+\]\((?!(?:https?://|src/))([^)#\s]+\.(?:md|yaml))[^)]*\)'
)

# Maximum size for a markdown file to prevent DoS via OOM.
MAX_MD_FILE_SIZE = 1 * 1024 * 1024  # 1MB


def _IsSafePath(input_api, path, root):
    """Normalizes and validates that a path is within a specific root."""
    norm_path = input_api.os_path.normpath(path)
    if norm_path == root:
        return True
    return norm_path.startswith(root + input_api.os_path.sep)


def _FileError(output_api, affected_file, message):
    """Returns a PresubmitError prefixed with the file path."""
    return output_api.PresubmitError(
        f'File {affected_file.LocalPath()} {message}'
    )


def _FileWarning(output_api, affected_file, message):
    """Returns a PresubmitPromptWarning prefixed with the file path."""
    return output_api.PresubmitPromptWarning(
        f'File {affected_file.LocalPath()} {message}'
    )


def _ValidateSchema(output_api, f, content, active_schema):
    """Validates JSON content against basic properties in active_schema."""
    results = []
    required_keys = set(active_schema.get('required', []))
    properties = active_schema.get('properties', {})

    missing_keys = required_keys - set(content.keys())
    if missing_keys:
        results.append(
            _FileError(
                output_api,
                f,
                f"is missing required keys: {', '.join(sorted(missing_keys))}",
            )
        )

    for key, value in content.items():
        if key in properties:
            expected_type = properties[key].get('type')
            if expected_type == 'integer' and (
                isinstance(value, bool) or not isinstance(value, int)
            ):
                results.append(
                    _FileError(output_api, f, f"key '{key}' should be integer.")
                )
            elif expected_type == 'boolean' and not isinstance(value, bool):
                results.append(
                    _FileError(output_api, f, f"key '{key}' should be boolean.")
                )
            elif expected_type == 'array':
                if not isinstance(value, list):
                    results.append(
                        _FileError(
                            output_api, f, f"key '{key}' should be array."
                        )
                    )
                else:
                    items_schema = properties[key].get('items', {})
                    if (
                        isinstance(items_schema, dict)
                        and items_schema.get('type') == 'string'
                    ):
                        for item in value:
                            if not isinstance(item, str):
                                results.append(
                                    _FileError(
                                        output_api,
                                        f,
                                        f"key '{key}' list contains a "
                                        f"non-string element: {item}",
                                    )
                                )
            elif expected_type == 'string' and not isinstance(value, str):
                results.append(
                    _FileError(output_api, f, f"key '{key}' should be string.")
                )

            expected_enum = properties[key].get('enum')
            if expected_enum and value not in expected_enum:
                results.append(
                    _FileError(
                        output_api,
                        f,
                        f"key '{key}' must be one of {expected_enum}.",
                    )
                )
    return results


def CheckMarkdownFiles(input_api, output_api):
    results = []
    repo_root = input_api.change.RepositoryRoot()
    skill_dir = input_api.PresubmitLocalPath()

    def FileFilter(affected_file):
        return input_api.FilterSourceFile(
            affected_file,
            files_to_check=(r'.*\.md$',),
        )

    # 1. Map affected files by absolute path for O(1) lookups.
    affected_files_map = {
        f.AbsoluteLocalPath(): f
        for f in input_api.AffectedFiles(
            file_filter=FileFilter, include_deletes=True
        )
    }

    if not affected_files_map:
        return []

    # 2. Identify all markdown files in the directory for reachability.
    all_markdown_files = set()
    for root, _, files in os.walk(skill_dir):
        for file in files:
            # Skip README.md as it is intended for humans and not part of the
            # agent's operational graph.
            if file.endswith('.md') and file != 'README.md':
                abs_path = input_api.os_path.normpath(
                    input_api.os_path.join(root, file)
                )
                # Skip files that are currently being deleted.
                if (
                    abs_path in affected_files_map
                    and affected_files_map[abs_path].Action() == 'D'
                ):
                    continue
                all_markdown_files.add(abs_path)

    # 3. Process every file to build the graph and check formatting.
    # Adjacency list: node (abs path) -> list of connected nodes (abs paths)
    graph = {node: [] for node in all_markdown_files}
    checked_existence = {}

    for md_file in all_markdown_files:
        if input_api.os_path.getsize(md_file) > MAX_MD_FILE_SIZE:
            results.append(
                output_api.PresubmitError(
                    f'File {md_file} exceeds max size 1MB (DoS mitigation).'
                )
            )
            continue

        is_modified = md_file in affected_files_map
        content = None

        if is_modified:
            content = input_api.ReadFile(affected_files_map[md_file])
        else:
            try:
                with open(md_file, 'r', encoding='utf-8') as f:
                    content = f.read()
            except IOError:
                continue

        if not content:
            continue

        # Scenario 2: Trailing Newlines (Modified files only)
        if is_modified:
            if (
                not content.endswith('\n')
                or content.endswith(('\n\n', '\r\n\r\n'))
                or '\r' in content
            ):
                results.append(
                    output_api.PresubmitError(
                        f'File {affected_files_map[md_file].LocalPath()} '
                        'must use Unix line endings (\\n) and end with '
                        'exactly one newline.'
                    )
                )

        lines = content.splitlines(True)
        in_fenced_block = False
        in_indented_block = False
        prev_line_empty = True

        for line_num, line in enumerate(lines, start=1):
            line_stripped = line.rstrip('\r\n')

            # Fenced code block detection
            if line_stripped.lstrip().startswith(('```', '~~~')):
                in_fenced_block = not in_fenced_block
                continue

            # Indented code block detection
            if not line_stripped.strip():
                prev_line_empty = True
                continue

            if prev_line_empty and (
                line.startswith('    ') or line.startswith('\t')
            ):
                in_indented_block = True
            elif not (line.startswith('    ') or line.startswith('\t')):
                in_indented_block = False

            prev_line_empty = False

            if in_fenced_block or in_indented_block:
                continue

            # Scenario 1: 80-Character Limit (Modified files only)
            if is_modified:
                line_len = len(line_stripped)
                for match in EXEMPT_TOKENS_RE.finditer(line_stripped):
                    line_len -= len(match.group(0))
                if line_len > 80:
                    results.append(
                        output_api.PresubmitPromptWarning(
                            f'Line {line_num} in '
                            f'{affected_files_map[md_file].LocalPath()} '
                            f'exceeds 80 characters '
                            f'({len(line_stripped)} chars):\n'
                            f'{line_stripped[:40]}... (truncated)'
                        )
                    )

            # Scenario 3: Link Extraction & Validation
            for match in EXEMPT_TOKENS_RE.finditer(line):
                full_path = None
                token = match.group(0)

                # Group 1: Absolute src/...
                if match.group(1):
                    token = match.group(1).rstrip('.')
                    full_path = input_api.os_path.normpath(
                        input_api.os_path.join(repo_root, token[4:])
                    )
                # Group 3: Relative link
                elif match.group(3):
                    token = match.group(3)
                    full_path = input_api.os_path.normpath(
                        input_api.os_path.join(
                            input_api.os_path.dirname(md_file), token
                        )
                    )

                if not full_path:
                    continue

                # Security: Prevent path traversal
                if not _IsSafePath(input_api, full_path, repo_root):
                    msg = (
                        f'Line {line_num} in '
                        f'{input_api.os_path.relpath(md_file, repo_root)} '
                        f'attempts path traversal: {token}'
                    )
                    if is_modified:
                        results.append(output_api.PresubmitError(msg))
                    else:
                        results.append(output_api.PresubmitPromptWarning(msg))
                    continue

                # Add to reachability graph (only for local markdown files)
                if full_path.endswith('.md') and _IsSafePath(
                    input_api, full_path, skill_dir
                ):
                    graph[md_file].append(full_path)

                # Existence Check (Validate EVERY link in graph to catch
                # deletion-breaks)
                if full_path not in checked_existence:
                    checked_existence[full_path] = input_api.os_path.exists(
                        full_path
                    )

                if not checked_existence[full_path]:
                    is_active = is_modified or full_path in affected_files_map
                    msg = (
                        f'Line {line_num} in '
                        f'{input_api.os_path.relpath(md_file, repo_root)} '
                        f'references a non-existent file: {token}'
                    )
                    if is_active:
                        results.append(output_api.PresubmitError(msg))
                    else:
                        results.append(output_api.PresubmitPromptWarning(msg))

    # Scenario 4: Reachability (BFS from SKILL.md)
    skill_md_path = input_api.os_path.normpath(
        input_api.os_path.join(skill_dir, 'SKILL.md')
    )
    if skill_md_path not in graph:
        results.append(
            output_api.PresubmitError(
                f'Critical Error: Entry point {skill_md_path} is missing.'
            )
        )
        return results

    visited = set()
    queue = collections.deque([skill_md_path])
    while queue:
        node = queue.popleft()
        if node not in visited:
            visited.add(node)
            for neighbor in graph.get(node, []):
                if neighbor in all_markdown_files and neighbor not in visited:
                    queue.append(neighbor)

    for md_file in all_markdown_files:
        if md_file not in visited:
            rel_path = input_api.os_path.relpath(md_file, repo_root)
            is_active_violation = md_file in affected_files_map

            msg = (
                f'Unreachable Markdown File: {rel_path} cannot be reached '
                'from SKILL.md. Even if it links to another file, it is '
                'part of an isolated cycle. Please add a link to it in '
                'ROUTING.md or another connected document.'
            )

            if is_active_violation:
                results.append(output_api.PresubmitError(msg))
            else:
                results.append(output_api.PresubmitPromptWarning(msg))

    # Scenario 5: Content mandates
    if skill_md_path in affected_files_map:
        skill_content = input_api.ReadFile(affected_files_map[skill_md_path])
        if 'Tone Mandate (Signal-to-Noise)' not in skill_content:
            results.append(
                output_api.PresubmitError(
                    'File SKILL.md must contain the "Tone Mandate '
                    '(Signal-to-Noise)" section.'
                )
            )
        elif (
            'Zero Preamble/Postamble' not in skill_content
            or 'Artifacts Only' not in skill_content
        ):
            results.append(
                output_api.PresubmitError(
                    'File SKILL.md Tone Mandate must explicitly enforce '
                    '"Zero Preamble/Postamble" and "Artifacts Only".'
                )
            )

    return results


def CheckJsonFiles(input_api, output_api):
    results = []

    skill_dir = input_api.PresubmitLocalPath()
    schema_path = input_api.os_path.join(skill_dir, 'workflow_schema.json')

    affected_files_map = {
        af.AbsoluteLocalPath(): af
        for af in input_api.AffectedFiles(include_deletes=False)
    }

    schema_content_str = None
    if schema_path in affected_files_map:
        schema_content_str = input_api.ReadFile(affected_files_map[schema_path])

    if schema_content_str is None:
        try:
            with open(schema_path, 'r', encoding='utf-8') as f:
                schema_content_str = f.read()
        except IOError:
            pass

    if not schema_content_str:
        return []

    try:
        schema = json.loads(schema_content_str)
    except ValueError as e:
        results.append(
            output_api.PresubmitError(f'Invalid workflow_schema.json: {e}')
        )
        return results

    if not isinstance(schema, dict):
        results.append(
            output_api.PresubmitError(
                'workflow_schema.json must be a JSON object.'
            )
        )
        return results

    state_block_schema = schema.get('definitions', {}).get('StateBlock', {})
    project_spec_schema = schema.get('definitions', {}).get('ProjectSpec', {})
    review_feedback_schema = schema.get('definitions', {}).get(
        'ReviewFeedback', {}
    )
    constraints_schema = schema.get('definitions', {}).get('Constraints', {})
    persona_def_schema = schema.get('definitions', {}).get('PersonaDef', {})

    # Load check_json_format.py once for efficient reuse.
    check_json_format = None
    if not getattr(input_api, 'is_test', False):
        this_dir = input_api.PresubmitLocalPath()
        check_json_script = input_api.os_path.join(
            this_dir, 'check_json_format.py'
        )
        try:
            spec = importlib.util.spec_from_file_location(
                'check_json_format', check_json_script
            )
            if spec:
                mod = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(mod)
                check_json_format = mod
        except (ImportError, AttributeError, FileNotFoundError):
            pass

    def FileFilter(affected_file):
        return input_api.FilterSourceFile(
            affected_file,
            files_to_check=(
                r'.*(state_block|project|review(\..+)?|constraints)'
                r'\.workflow(\.\d+)?\.json$',
                r'.*personas/.*\.json$',
            ),
        )

    for f in input_api.AffectedFiles(
        file_filter=FileFilter, include_deletes=False
    ):
        content_str = input_api.ReadFile(f)
        if not content_str.strip():
            continue
        try:
            content = json.loads(content_str)
        except ValueError as e:
            results.append(
                output_api.PresubmitError(
                    f'File {f.LocalPath()} is not valid JSON: {e}'
                )
            )
            continue

        if not isinstance(content, dict):
            results.append(
                output_api.PresubmitError(
                    f'File {f.LocalPath()} must be a JSON object.'
                )
            )
            continue

        filename = input_api.os_path.basename(f.LocalPath())
        parts = f.LocalPath().replace('\\', '/').split('/')
        if filename.startswith('state_block'):
            active_schema = state_block_schema
        elif filename.startswith('project'):
            active_schema = project_spec_schema
        elif filename.startswith('review'):
            active_schema = review_feedback_schema
        elif filename.startswith('constraints'):
            active_schema = constraints_schema
        elif 'personas' in parts:
            active_schema = persona_def_schema
            # Enforce naming convention: avoid redundant "_expert" suffix.
            if filename.endswith('_expert.json'):
                results.append(
                    output_api.PresubmitError(
                        f'Persona file {f.LocalPath()} uses the redundant '
                        '"_expert" suffix. Please use a concise name '
                        '(e.g., "security.json" instead of '
                        '"security_expert.json").'
                    )
                )
            # Enforce directory depth limit (max 5 from /personas)
            # Depth is exactly the index of 'personas' in the reversed list
            depth = parts[::-1].index('personas')
            if depth > 5:
                results.append(
                    output_api.PresubmitError(
                        f'File {f.LocalPath()} exceeds maximum persona '
                        f'directory depth of 5 (current depth: {depth})'
                    )
                )

            # Cold Logic Static Analysis: Enforce imperative mandates
            mandate = content.get('mandate')
            if isinstance(mandate, list) and mandate:
                first_line = mandate[0]
                if not first_line.startswith('MANDATE:'):
                    results.append(
                        output_api.PresubmitError(
                            f'File {f.LocalPath()} mandate must start with '
                            '"MANDATE:" in accordance with Cold Logic.'
                        )
                    )

                # Check for conversational filler or role-playing
                filler_patterns = [
                    r'\bI am\b',
                    r'\bAs a\b',
                    r'\bMy role\b',
                    r'\bYour goal\b',
                    r'\bYou are\b',
                    r'\bPlease\b',
                    r'\bThank you\b',
                ]
                combined_mandate = " ".join(mandate)
                for pattern in filler_patterns:
                    if re.search(pattern, combined_mandate, re.IGNORECASE):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} contains '
                                'conversational filler or role-playing '
                                f'vestige: "{pattern}"'
                            )
                        )
        else:
            continue

        results.extend(_ValidateSchema(output_api, f, content, active_schema))

        # 2.5 Formatting & Joining Rule Validation
        if check_json_format:
            fmt_ok, lint_errs = check_json_format.CheckFormatting(
                f.AbsoluteLocalPath(), fix=False
            )
            if not fmt_ok:
                local_check_script = input_api.os_path.join(
                    input_api.PresubmitLocalPath(), 'check_json_format.py'
                )
                results.append(
                    output_api.PresubmitError(
                        f'File {f.LocalPath()} has incorrect JSON '
                        f'formatting. Please run: python3 {local_check_script} '
                        '--fix'
                    )
                )
            for err in lint_errs:
                results.append(output_api.PresubmitError(f'LINT: {err}'))

        # 3. Checklist Validation (Correctness & Schema)
        checklist = content.get('checklist')
        if checklist is not None:
            if not isinstance(checklist, dict):
                results.append(
                    output_api.PresubmitError(
                        f'File {f.LocalPath()} key "checklist" '
                        'must be an object.'
                    )
                )
            else:
                is_persona = active_schema is persona_def_schema
                for k, v in checklist.items():
                    if is_persona:
                        if not isinstance(v, (str, list)):
                            results.append(
                                output_api.PresubmitError(
                                    f'File {f.LocalPath()} persona '
                                    f'checklist key "{k}" must have a '
                                    f'string or list description, got '
                                    f'{type(v).__name__}'
                                )
                            )
                        elif isinstance(v, list):
                            for i, item in enumerate(v):
                                if not isinstance(item, str):
                                    results.append(
                                        output_api.PresubmitError(
                                            f'File {f.LocalPath()} persona '
                                            f'checklist key "{k}" list '
                                            f'item at index {i} must be a '
                                            f'string, got '
                                            f'{type(item).__name__}'
                                        )
                                    )
                            # Token Merging Check: ensure joining won't create
                            # semantic errors.
                            if any(
                                not v[j].endswith(tuple(".,!?;:"))
                                and not v[j].endswith(" ")
                                and j < len(v) - 1
                                for j in range(len(v))
                            ):
                                results.append(
                                    output_api.PresubmitPromptWarning(
                                        f'File {f.LocalPath()} persona key '
                                        f'"{k}" uses an array but some items '
                                        'lack trailing spaces. Ensure your '
                                        'Orchestrator uses a space-join '
                                        'strategy to prevent token merging.'
                                    )
                                )
                    else:
                        if not isinstance(v, bool):
                            results.append(
                                output_api.PresubmitError(
                                    f'File {f.LocalPath()} checklist key "{k}" '
                                    f'must be a boolean, got {type(v).__name__}'
                                )
                            )

        # 4. Decision Graph Validation
        next_p = content.get('next_stage')

        if filename.startswith('state_block'):
            # Oscillation and Conflict Report Validation
            oscillation = content.get('oscillation_detected')
            conflict_report = content.get('conflict_report', [])
            if oscillation is True:
                if not conflict_report:
                    results.append(
                        output_api.PresubmitError(
                            f'File {f.LocalPath()} has oscillation_detected: '
                            'true but conflict_report is empty.'
                        )
                    )
                if next_p != 'ESCALATION':
                    results.append(
                        output_api.PresubmitError(
                            f'File {f.LocalPath()} has oscillation_detected: '
                            'true but next_stage is not ESCALATION.'
                        )
                    )

        elif filename.startswith('project'):
            if 'environment' in content:
                environment = content['environment']
                if not isinstance(environment, dict):
                    results.append(
                        output_api.PresubmitError(
                            f'File {f.LocalPath()} key "environment" '
                            'must be an object.'
                        )
                    )
                else:
                    vcs = environment.get('vcs')
                    harness = environment.get('harness')
                    if vcs not in ('GIT', 'JJ'):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment.vcs '
                                f'must be GIT or JJ, got {vcs}'
                            )
                        )
                    if harness not in ('JETSKI', 'GENERIC_CLI'):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment.harness '
                                f'must be JETSKI or GENERIC_CLI, '
                                f'got {harness}'
                            )
                        )
                    repo_type = environment.get('repo_type')
                    output_directory = environment.get('output_directory')

                    if 'repo_type' not in environment:
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment is '
                                'missing required key "repo_type".'
                            )
                        )
                    elif repo_type not in ('CHROMIUM', 'GOOGLE_INTERNAL'):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment.repo_type '
                                'must be CHROMIUM or GOOGLE_INTERNAL, '
                                f'got {repo_type}'
                            )
                        )

                    if 'output_directory' not in environment:
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment is '
                                'missing required key "output_directory".'
                            )
                        )
                    elif not isinstance(output_directory, str):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} '
                                'environment.output_directory must be '
                                f'a string, got '
                                f'{type(output_directory).__name__}'
                            )
                        )

                    temp_directory = environment.get('temp_directory')
                    if 'temp_directory' not in environment:
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} environment is '
                                'missing required key "temp_directory".'
                            )
                        )
                    elif not isinstance(temp_directory, str):
                        results.append(
                            output_api.PresubmitError(
                                f'File {f.LocalPath()} '
                                'environment.temp_directory must be '
                                f'a string, got '
                                f'{type(temp_directory).__name__}'
                            )
                        )

        elif filename.startswith('constraints'):
            if next_p and next_p not in [
                'SYNTHESIS',
                'VALIDATION',
                'ESCALATION',
            ]:
                results.append(
                    output_api.PresubmitError(
                        f'File {f.LocalPath()} must signal '
                        f'SYNTHESIS, VALIDATION, or ESCALATION, not {next_p}'
                    )
                )

    return results


def CheckTestJsonFiles(input_api, output_api):
    results = []
    skill_dir = input_api.PresubmitLocalPath()
    expected_dir = input_api.os_path.join(skill_dir, 'tests')

    def FileFilter(affected_file):
        absolute_path = affected_file.AbsoluteLocalPath()
        filename = input_api.os_path.basename(absolute_path)
        return input_api.os_path.dirname(
            absolute_path
        ) == expected_dir and bool(
            re.match(r'^workflow_stage_.*_tests\.json$', filename)
        )

    for f in input_api.AffectedFiles(
        file_filter=FileFilter, include_deletes=False
    ):
        content_str = input_api.ReadFile(f)
        if not content_str.strip():
            continue

        try:
            content = json.loads(content_str)
        except ValueError as e:
            results.append(
                output_api.PresubmitError(
                    f"File {f.LocalPath()} is not valid JSON: {e}"
                )
            )
            continue

        if not isinstance(content, dict):
            results.append(
                output_api.PresubmitError(
                    f"File {f.LocalPath()} must be a JSON object."
                )
            )
            continue

        required_scenario_keys = ["name", "base_inputs", "cases"]
        for key in required_scenario_keys:
            if key not in content:
                results.append(
                    output_api.PresubmitError(
                        f"File {f.LocalPath()} is missing required key: {key}"
                    )
                )

        cases = content.get("cases", [])
        if not isinstance(cases, list):
            results.append(
                output_api.PresubmitError(
                    f"File {f.LocalPath()} key 'cases' must be an array."
                )
            )
            continue

        for idx, case in enumerate(cases):
            if not isinstance(case, dict):
                results.append(
                    output_api.PresubmitError(
                        f"File {f.LocalPath()} case at index {idx} "
                        "must be an object."
                    )
                )
                continue

            required_case_keys = ["name", "expected_outputs"]
            for key in required_case_keys:
                if key not in case:
                    results.append(
                        output_api.PresubmitError(
                            f"File {f.LocalPath()} case "
                            f"'{case.get('name', idx)}' "
                            f"is missing required key: {key}"
                        )
                    )

            override_inputs = case.get("override_inputs", {})
            if not isinstance(override_inputs, dict):
                results.append(
                    output_api.PresubmitError(
                        f"File {f.LocalPath()} case '{case.get('name', idx)}' "
                        f"key 'override_inputs' must be an object."
                    )
                )
                continue

            allowed_overrides = [
                "project_spec_overrides",
                "state_block_overrides",
                "draft_files_overrides",
                "mock_reviews",
            ]
            for key in override_inputs.keys():
                if key not in allowed_overrides:
                    results.append(
                        output_api.PresubmitError(
                            f"File {f.LocalPath()} case "
                            f"'{case.get('name', idx)}' "
                            f"key 'override_inputs' contains invalid "
                            f"property: {key}. "
                            f"Allowed properties are: "
                            f"{', '.join(allowed_overrides)}"
                        )
                    )

    return results


def CheckTempDirectory(input_api, output_api):
    results = []
    for f in input_api.AffectedFiles(include_deletes=False):
        if '.temp/' in f.LocalPath().replace('\\', '/'):
            results.append(
                output_api.PresubmitError(
                    f'File {f.LocalPath()} is in the .temp/ directory, '
                    f'which must be excluded from all CLs.'
                )
            )
    return results


def CheckChangeOnUpload(input_api, output_api):
    results = []
    results.extend(CheckMarkdownFiles(input_api, output_api))
    results.extend(CheckJsonFiles(input_api, output_api))
    results.extend(CheckTestJsonFiles(input_api, output_api))
    results.extend(CheckTempDirectory(input_api, output_api))
    return results


def CheckChangeOnCommit(input_api, output_api):
    results = []
    results.extend(CheckMarkdownFiles(input_api, output_api))
    results.extend(CheckJsonFiles(input_api, output_api))
    results.extend(CheckTestJsonFiles(input_api, output_api))
    results.extend(CheckTempDirectory(input_api, output_api))
    return results
