Claude Code Plugins

Community-maintained marketplace

Feedback
1
0

Enforce strict legacy-Laravel rules when editing npid-api-layer FastAPI code - prevents modernization and ensures translator pattern compliance

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name npid-fastapi-skill
description Enforce strict legacy-Laravel rules when editing npid-api-layer FastAPI code - prevents modernization and ensures translator pattern compliance

NPID FastAPI Layer - Legacy-Laravel Enforcement Skill

MANDATORY PRE-IMPLEMENTATION CHECKLIST

Before writing ANY code in npid-api-layer/, you MUST:

  • Verify working directory is npid-api-layer/
  • Check Python reference implementation first (src/python/npid_api_client.py)
  • Review existing translator methods in app/translators/legacy.py
  • Confirm you understand the translator pattern
  • NEVER bypass the translator pattern

If ANY checkbox is unchecked, STOP and complete it before proceeding.


1. CRITICAL INVARIANTS

All Laravel Write Calls MUST Use:

Form Encoding: application/x-www-form-urlencoded (NEVER application/json) ✅ AJAX Header: X-Requested-With: XMLHttpRequest (ALWAYS required) ✅ CSRF Token: Fresh _token scraped from form/page per request ✅ Session Cookies: Active session from ~/.npid_session.pklParameter Names: Exact names Laravel expects (snake_case, NOT camelCase) ✅ Session Wrapper: Use session.post() method (auto-injects CSRF token)

Response Handling Rules:

Laravel responses are unpredictable:

  • May return HTML, JSON, or nested JSON strings
  • May return different formats for same endpoint based on headers
  • Parse HTML with BeautifulSoup/regex (see legacy.py:144-184)
  • Handle nested response garbage (see legacy.py:97-141)
  • NEVER assume JSON format without checking

Parameter Naming Convention:

ALWAYS use Laravel's exact parameter names:

  • athlete_id (NOT athleteId)
  • sport_alias (NOT sportAlias)
  • video_type (NOT videoType)
  • athlete_main_id (NOT athleteMainId)
  • video_msg_id (NOT videoMsgId)

Documented exceptions (quirky Laravel fields):

  • newVideoLink (CamelCase)
  • newVideoSeason (CamelCase, always empty)
  • schoolinfo[add_video_season] (Array notation for actual season)

TypeScript ↔ FastAPI ↔ Laravel Value Conversion

CRITICAL: Pydantic does NOT auto-convert enum values

Pattern (VERIFIED 2025-12-07):

TypeScript (snake_case) → FastAPI Enum (snake_case) → Translator (converts) → Laravel (Title Case)

Example - Video Stage:

  1. TypeScript sends: { stage: "on_hold" }
  2. Pydantic Enum accepts: VideoStage.ON_HOLD = "on_hold"
  3. Translator converts: "on_hold" → "On Hold"
  4. Laravel receives: video_progress_stage: "On Hold"

If enum values don't match TypeScript:

  • Result: 422 Unprocessable Entity
  • Cause: Pydantic validates "on_hold" against enum values, finds no match
  • Fix: Enum must use snake_case values, translator converts to Title Case

Before implementing ANY enum field:

  • Check TypeScript code: What format does it send?
  • Check Python client: What format does Laravel expect?
  • Enum values match TypeScript format (NOT Laravel)
  • Translator converts enum.value to Laravel format

Verification:

# Check TypeScript normalization
grep -A 10 "normalizeStage\|normalizeStatus" src/video-progress.tsx

# Check Python normalization
grep -A 10 "_normalize.*for_api" src/python/npid_api_client.py

# Check translator conversion
grep -A 10 "stage_map\|status_map" npid-api-layer/app/translators/legacy.py

Video Progress Caching Strategy

Problem: Laravel /videoteammsg/videoprogress returns 1699+ tasks (slow query)

Solution: Local SQLite cache with optimistic updates

Cache Location: ~/.prospect-pipeline/video-progress-cache.sqlite

Pattern (src/lib/video-progress-cache.ts):

// On initial load: Fetch from API → Update cache
await upsertTasks(tasks);

// On status/stage/due date change:
// 1. Update cache immediately (optimistic)
await updateCachedTaskStatusStage(task.id, { stage: newStage });

// 2. Update UI from cache (instant feedback)
const updated = await getCachedTasks();
setTasks(updated);

// 3. Send update to API (background)
await apiFetch(`/video/${task.id}/stage`, {...});

// 4. NO need to reload all 1699 tasks

Current Issue (2025-12-07):

  • video-progress.tsx calls loadTasks() after every change
  • Reloads ALL 1699 tasks from Laravel (slow)
  • Cache exists but isn't used for updates

Fix:

// Instead of:
onStatusUpdate();  // → loadTasks() → fetch all 1699 tasks

// Do:
// 1. Update cache
await updateCachedTaskStatusStage(task.id, { stage: newStage });

// 2. Update local state
setTasks(tasks.map(t =>
  t.id === task.id ? { ...t, stage: newStage } : t
));

// 3. Optional: Background sync
// (only if you need to validate Laravel accepted it)

Benefits:

  • Instant UI updates (no 5-second Laravel query)
  • Works offline
  • Reduces Laravel load
  • Cache invalidation: 30-minute TTL or manual refresh

2. THE TRANSLATOR PATTERN

Core Architecture

The npid-api-layer project is a translation layer:

Raycast Extension (clean JSON) → FastAPI Layer → Legacy Laravel (form-encoded, HTML/JSON hybrid)

Single Source of Truth:

  • app/translators/legacy.py - ALL Laravel interaction logic
  • app/session.py - Session management, CSRF, auth headers
  • app/routers/*.py - Clean FastAPI endpoints (MUST use translator)

The Pattern - MUST Follow Exactly:

from app.translators.legacy import LegacyTranslator

translator = LegacyTranslator()

# Step 1: Request translation (clean → legacy)
endpoint, form_data = translator.{method}_to_legacy(clean_request)

# Step 2: Execute via session (handles CSRF, headers, cookies)
response = await session.post(endpoint, data=form_data)

# Step 3: Response parsing (legacy → clean)
result = translator.parse_{method}_response(response.text)

What This Pattern Achieves:

  1. Isolation: All Laravel quirks in ONE file (legacy.py)
  2. Maintainability: Laravel changes = update ONE method
  3. Testability: Mock translator, not scattered logic
  4. Documentation: Parameter names documented in translator
  5. Type Safety: Pydantic models at API boundary

3. FORBIDDEN ACTIONS

❌ NEVER Do These:

DO NOT bypass translator:

# ❌ WRONG - Inline form construction
form_data = {
    "_token": session.csrf_token,
    "athlete_id": payload.athlete_id
}
response = await client.post(url, data=form_data)

DO NOT use JSON bodies:

# ❌ WRONG - Laravel won't accept JSON
response = await client.post(url, json=payload.dict())

DO NOT inline HTML parsing:

# ❌ WRONG - Parsing in router
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')

DO NOT bypass session wrapper:

# ❌ WRONG - Direct client usage
response = await client.post(url, data=form_data)

DO NOT rename parameters "for consistency":

# ❌ WRONG - Laravel expects snake_case
form_data = {"athleteId": id}  # Laravel won't recognize this

DO NOT assume JSON responses:

# ❌ WRONG - May return HTML
result = response.json()  # Will fail if HTML returned

DO NOT remove HTML scrapers:

# ❌ WRONG - "Modernizing" by removing parsing
# Comment: "Removed HTML parsing, endpoint now returns JSON"
# Reality: Endpoint still returns HTML, code broken

DO NOT add REST abstractions:

# ❌ WRONG - Laravel is NOT RESTful
# Don't create generic REST clients, bearer auth, JWT tokens, etc.

Complete Forbidden List:

❌ NEVER generate code that allows Raycast to contact Laravel directly ❌ NEVER use application/json content type for Laravel POSTs ❌ NEVER remove HTML scrapers ("modernize" to JSON assumptions) ❌ NEVER bypass session wrapper for direct client.post() ❌ NEVER add REST-style abstractions or "clean" parameter names ❌ NEVER use bearer auth, JWT, or modern auth patterns ❌ NEVER assume HTML response means "session expired" ❌ NEVER inline form data construction in routers ❌ NEVER inline HTML parsing in routers ❌ NEVER use json= parameter in POST requests


4. SKILL ABILITIES

fix-seasons-endpoint

Problem:

  • File: npid-api-layer/app/routers/video.py:41-122
  • The /seasons proxy endpoint bypasses LegacyTranslator
  • Duplicates HTML parsing logic inline

Solution:

  • Refactor endpoint to use translator.seasons_request_to_legacy()
  • Use translator.parse_seasons_response() for parsing
  • Remove inline BeautifulSoup usage

Verification:

# Should find NO inline form construction in /seasons endpoint
grep -A 20 "@router.api_route\(\"/seasons\"" npid-api-layer/app/routers/video.py | grep "form_data = {"

# Should find translator usage
grep -A 20 "@router.api_route\(\"/seasons\"" npid-api-layer/app/routers/video.py | grep "LegacyTranslator"

fix-duplicate-logic

Problem:

  • HTML parsing logic duplicated across router files
  • BeautifulSoup imported in routers (should only be in translator)

Solution:

  • Move all parsing logic to LegacyTranslator class
  • Remove BeautifulSoup imports from routers
  • Add translator methods for any new parsing needs

Verification:

# Should find NO BeautifulSoup imports in routers
grep -n "from bs4 import BeautifulSoup" npid-api-layer/app/routers/*.py
grep -n "import BeautifulSoup" npid-api-layer/app/routers/*.py

verify-session-stack

Checks to perform:

  1. Session loading:

    • File: npid-api-layer/app/session.py:59-78
    • Loads from ~/.npid_session.pkl
    • Fallback to credential login if session missing
  2. CSRF token refresh:

    • File: npid-api-layer/app/session.py:191-202
    • Fetches from /auth/login page
    • Regex: r'name="_token"\\s+value="([^"]+)"'
  3. AJAX header:

    • File: npid-api-layer/app/session.py:46-49
    • Global headers include X-Requested-With: XMLHttpRequest
  4. Auto-injection:

    • File: npid-api-layer/app/session.py:168-189
    • post() method auto-injects _token (line 178)
    • post() method auto-injects api_key if available (line 182)

enforce-legacy-rules

Static checks to block violations:

# Block: JSON bodies
grep -n "json=" npid-api-layer/app/routers/*.py

# Block: Direct client.post (should use session.post)
grep -n "client\.post" npid-api-layer/app/routers/*.py | grep -v "session"

# Block: Inline form construction
grep -n "form_data = {" npid-api-layer/app/routers/*.py

# Require: Translator usage
grep -n "LegacyTranslator()" npid-api-layer/app/routers/*.py

verify-endpoint-correctness

Verify endpoints match Python reference:

Endpoint Purpose Expected URL Python Reference
Video submit /athlete/update/careervideos/{athlete_id} src/python/npid_api_client.py:859
Stage update /API/scout-api/video-stage src/python/npid_api_client.py:803
Seasons fetch /API/scout-api/video-seasons-by-video-type src/python/npid_api_client.py:692

Verification command:

# Check all endpoint URLs in translator
grep -n '"/' npid-api-layer/app/translators/legacy.py | grep endpoint

5. REFERENCE FILES

MUST CHECK Before Implementing:

Python Reference (PRIMARY SOURCE):

  • src/python/npid_api_client.py (lines 690-960)
    • Video submission: lines 859-873
    • Stage update: lines 803-840
    • Seasons fetch: lines 692-714

Skills Documentation:

  • .claude/skills/npid-api-calls.md - HTTP header requirements
  • .claude/skills/npid-video-submission.md - Video workflow
  • .claude/skills/npid-athlete-main-id.md - athlete_main_id extraction

API Specs:

  • NPID-API-specs/ATHLETE_MAIN_ID_INVARIANT.md - athlete_main_id rules
  • docs/plans/2025-11-14-npid-athlete-search-design.md - Design docs

MUST FOLLOW Patterns From:

Core Implementation:

  • npid-api-layer/app/translators/legacy.py - All translation logic
  • npid-api-layer/app/session.py - Session/CSRF/headers
  • npid-api-layer/app/models/schemas.py - Clean API contracts

Current Routers:

  • npid-api-layer/app/routers/video.py - Video operations
  • npid-api-layer/app/routers/athlete.py - Athlete resolution
  • npid-api-layer/app/routers/assignments.py - Assignment fetching

Documentation:

  • npid-api-layer/README.md - Project architecture

6. CORRECT WORKFLOW

When user asks to add/modify NPID API functionality:

Step 1: Research Phase

  1. Read this skill (you're here now)
  2. Check Python reference:
    # Find the endpoint implementation
    grep -n "endpoint_keyword" src/python/npid_api_client.py
    
  3. Check existing translator:
    # See if method already exists
    grep -n "def.*to_legacy" npid-api-layer/app/translators/legacy.py
    

Step 2: Implementation Phase

If new endpoint needed:

  1. Add translation method to LegacyTranslator:

    @staticmethod
    def new_endpoint_to_legacy(request: NewRequest) -> Tuple[str, Dict[str, Any]]:
        """Convert clean request to legacy format."""
        endpoint = "/path/from/python/client"
        form_data = {
            # Extract exact field names from Python client
            "field_name": request.field
        }
        return endpoint, form_data
    
  2. Add response parser to LegacyTranslator:

    @staticmethod
    def parse_new_endpoint_response(raw_response: str) -> Dict[str, Any]:
        """Parse response (HTML, JSON, or nested)."""
        # Copy parsing logic from Python client
        pass
    
  3. Router calls translator methods only:

    @router.post("/new-endpoint")
    async def new_endpoint(request: Request, payload: NewRequest):
        session = get_session(request)
        translator = LegacyTranslator()
    
        endpoint, form_data = translator.new_endpoint_to_legacy(payload)
        response = await session.post(endpoint, data=form_data)
        result = translator.parse_new_endpoint_response(response.text)
    
        return NewResponse(**result)
    

If modifying existing:

  1. Change translator method, NOT router
  2. Verify all routers using that method still work
  3. Update response parser if format changed

Step 3: Verification Phase

# Run all verification commands (see section 8)
# Ensure no violations introduced

7. EXAMPLES

✅ GOOD - Follows Pattern Correctly

File: app/routers/video.py

@router.post("/submit", response_model=VideoSubmitResponse)
async def submit_video(request: Request, payload: VideoSubmitRequest):
    """
    Submit video to athlete profile.
    Clean endpoint → Translator → Legacy Laravel.
    """
    session = get_session(request)
    translator = LegacyTranslator()

    # Step 1: Translator converts clean request to legacy format
    endpoint, form_data = translator.video_submit_to_legacy(payload)

    logger.info(f"📤 Submitting video for athlete {payload.athlete_id}")

    # Step 2: Session handles CSRF, headers, cookies
    response = await session.post(endpoint, data=form_data)

    # Step 3: Translator parses response (handles nested JSON strings)
    result = translator.parse_video_submit_response(response.text)

    if result["success"]:
        return VideoSubmitResponse(
            success=True,
            message=result.get("message", "Video uploaded successfully"),
            athlete_id=payload.athlete_id,
            video_url=payload.video_url
        )
    else:
        raise HTTPException(status_code=400, detail=result.get("message"))

Why this is correct:

  • ✅ Uses LegacyTranslator for request/response translation
  • ✅ Uses session.post() (auto-handles CSRF, headers)
  • ✅ No inline form construction
  • ✅ No inline parsing logic
  • ✅ Clean Pydantic models at API boundary
  • ✅ All Laravel quirks isolated in translator

❌ BAD - Bypasses Pattern (NEVER DO THIS)

File: app/routers/video.py - WRONG IMPLEMENTATION

@router.post("/submit")
async def submit_video(request: Request, payload: VideoSubmitRequest):
    """WRONG: Bypasses translator pattern."""
    session = get_session(request)

    # ❌ WRONG: Inline form construction
    form_data = {
        "_token": session.csrf_token,  # Might forget this
        "athlete_id": payload.athlete_id,  # Is this the right param name?
        "video_url": payload.video_url,  # Laravel expects "newVideoLink"
        "season": payload.season  # Laravel expects "schoolinfo[add_video_season]"
    }

    # ❌ WRONG: Direct client usage (bypasses session wrapper)
    response = await client.post(
        "/athlete/update/careervideos",  # Missing athlete_id in path
        data=form_data,
        headers={"Content-Type": "application/x-www-form-urlencoded"}
        # ❌ Missing: X-Requested-With header
        # ❌ Missing: api_key injection
    )

    # ❌ WRONG: Inline parsing (assumes JSON, may be HTML)
    result = json.loads(response.text)  # Will crash if HTML returned

    # ❌ WRONG: May return nested JSON string, not parsed
    return result  # Might return {"data": {"response": "{\"success\":\"true\"}"}}

Why this is wrong:

  • ❌ Inline form construction (duplicates logic)
  • ❌ Parameter names may be incorrect (no reference to Python client)
  • ❌ Missing endpoint path parameters
  • ❌ Bypasses session wrapper (manual header management)
  • ❌ Missing AJAX header
  • ❌ Doesn't inject api_key
  • ❌ Assumes JSON response (Laravel may return HTML)
  • ❌ Doesn't handle nested JSON strings
  • ❌ Changes require editing router, not translator

✅ GOOD - Handles HTML Response

File: app/translators/legacy.py

@staticmethod
def parse_seasons_response(raw_response: str) -> Dict[str, Any]:
    """
    Parse seasons response.
    Laravel returns HTML <option> tags, NOT JSON.
    """
    try:
        # Try JSON first
        data = json.loads(raw_response)
        if data.get("status") == "ok" and "data" in data:
            return {"success": True, "seasons": data["data"]}
    except json.JSONDecodeError:
        pass

    # Fallback: Parse HTML
    seasons = []
    option_pattern = r'<option[^>]*value="([^"]*)"[^>]*>([^<]+)</option>'

    for match in re.finditer(option_pattern, raw_response):
        value, label = match.groups()
        if value:  # Skip empty placeholder
            seasons.append({
                "value": value,  # e.g., "highschool:18249"
                "label": label.strip()
            })

    return {
        "success": len(seasons) > 0,
        "seasons": seasons,
        "was_html": True
    }

Why this is correct:

  • ✅ Tries JSON first, falls back to HTML
  • ✅ Uses regex to parse HTML options
  • ✅ Returns normalized structure regardless of format
  • ✅ Isolated in translator (routers don't need to know about HTML)

❌ BAD - Assumes JSON (NEVER DO THIS)

File: app/routers/video.py - WRONG

@router.get("/seasons/{athlete_id}")
async def get_seasons(athlete_id: str):
    """WRONG: Assumes JSON response."""
    response = await session.post("/API/scout-api/video-seasons-by-video-type", data={...})

    # ❌ WRONG: Assumes JSON, but endpoint returns HTML
    seasons = response.json()  # Crashes with JSONDecodeError

    return {"seasons": seasons}

8. VERIFICATION COMMANDS

After ANY changes to npid-api-layer code, run these commands:

Check for Translator Pattern Violations

# Should return NO results (all POST calls should be session.post)
grep -n "\.post(" npid-api-layer/app/routers/*.py | grep -v "session.post"

Check for Inline Form Construction

# Should return NO results (form construction should be in translator)
grep -n "form_data = {" npid-api-layer/app/routers/*.py

Check for JSON Bodies (Forbidden)

# Should return NO results (Laravel requires form-encoding)
grep -n "json=" npid-api-layer/app/routers/*.py

Check for Inline HTML Parsing

# Should return NO results (parsing should be in translator)
grep -n "BeautifulSoup" npid-api-layer/app/routers/*.py

Verify Translator Methods Exist

# Should list all translation methods
grep -n "def.*to_legacy" npid-api-layer/app/translators/legacy.py

# Should list all parsing methods
grep -n "def parse_" npid-api-layer/app/translators/legacy.py

Verify Session Configuration

# Check AJAX header in global config
grep -n "X-Requested-With" npid-api-layer/app/session.py

# Check CSRF auto-injection
grep -n "_token" npid-api-layer/app/session.py

# Check api_key auto-injection
grep -n "api_key" npid-api-layer/app/session.py

Verify Endpoint URLs Match Python Client

# Extract endpoint URLs from translator
grep -n 'endpoint = "' npid-api-layer/app/translators/legacy.py

# Compare with Python client
grep -n 'endpoint.*=' src/python/npid_api_client.py | grep -E "(video|athlete|season)"

9. WHY THIS MATTERS

User's Pain Points (From CLAUDE.md)

The user has debugged this pattern 100+ times. Every time I:

  1. Don't check Python code first
  2. Make assumptions about HTML = expired session
  3. Add wrong error handling
  4. Break working code

This skill exists to stop that cycle.

Laravel's Quirks

Laravel's API is NOT RESTful. It's a legacy form-based system with:

Inconsistent Response Formats:

  • /videoteammsg/videoprogress returns HTML by default
  • Same endpoint returns JSON with X-Requested-With: XMLHttpRequest header
  • Video submit returns nested JSON strings: {"data": {"response": "{\"success\":\"true\"}"}}

Quirky Parameter Naming:

  • schoolinfo[add_video_season] - Array notation for season field
  • newVideoLink - CamelCase exception
  • newVideoSeason - Always empty (data goes to schoolinfo instead)
  • athleteviewtoken - Always empty string

CSRF Token Requirements:

  • Must be extracted from HTML page
  • Must be included in every POST as _token
  • Expires and needs refresh

AJAX Header Requirements:

  • X-Requested-With: XMLHttpRequest changes response format
  • Missing this header = HTML response instead of JSON
  • NOT a session expiration issue

Session Cookie Management:

  • 400-day persistent cookies from login
  • Loaded from ~/.npid_session.pkl
  • Must be included in all requests

Why Translator Pattern Exists

Problem without translator:

  • Laravel quirks scattered across 10+ router files
  • Parameter name changes require 10+ file edits
  • New engineer doesn't know quirks, breaks things
  • Testing requires mocking scattered logic

Solution with translator:

  • Laravel quirks in ONE file (legacy.py)
  • Parameter changes = ONE method edit
  • Clear documentation of exact field names
  • Easy to mock translator for testing
  • Routers stay clean and RESTful-looking

When Laravel changes, you fix ONE file, not ten.


10. COMMON VIOLATION SCENARIOS

Scenario 1: New Engineer Adds Endpoint

WRONG Approach:

# Thinks: "I'll just POST the data directly"
@router.post("/new-feature")
async def new_feature(data: dict):
    response = await client.post("/some/endpoint", json=data)
    return response.json()

CORRECT Approach:

  1. Check Python client for exact endpoint
  2. Add translator method for request/response
  3. Router calls translator only

Scenario 2: "Modernization" Attempt

WRONG Thinking:

  • "This HTML parsing is messy, endpoint probably returns JSON now"
  • "Let me remove this old BeautifulSoup code"
  • Result: Code breaks, endpoint still returns HTML

CORRECT Thinking:

  • "Endpoint returns HTML, this is documented"
  • "Keep parsing logic, move to translator if not there"
  • "Never assume Laravel changed without verification"

Scenario 3: Parameter Renaming

WRONG Thinking:

  • "JavaScript uses camelCase, let's be consistent"
  • Changes athlete_idathleteId in router
  • Result: Laravel doesn't recognize parameter, silent failure

CORRECT Thinking:

  • "Laravel expects exact parameter names"
  • "Python client documents correct names"
  • "Keep snake_case for Laravel parameters"

Scenario 4: Session Expiration Assumption

WRONG Thinking:

  • Sees HTML response instead of JSON
  • "Session must be expired, add login retry"
  • Result: Infinite login loop, actual issue is missing AJAX header

CORRECT Thinking:

  • "HTML response usually means missing X-Requested-With header"
  • "Check session.py for header configuration"
  • "Verify endpoint uses session.post() not client.post()"

Scenario 5: Bypass for "Quick Fix"

WRONG Thinking:

  • "Just need to change one field, translator is overkill"
  • Adds inline form construction in router
  • Result: Duplication, maintenance burden increases

CORRECT Thinking:

  • "Even small changes go through translator"
  • "Modify translator method, keep pattern consistent"
  • "Pattern overhead is tiny vs. maintenance cost"

QUICK REFERENCE CARD

Before ANY Code Change:

  1. ✅ Read Python reference (src/python/npid_api_client.py)
  2. ✅ Check existing translator methods
  3. ✅ Verify you understand the pattern
  4. ✅ Never bypass translator

The Pattern (Always):

translator = LegacyTranslator()
endpoint, form_data = translator.method_to_legacy(request)
response = await session.post(endpoint, data=form_data)
result = translator.parse_response(response.text)

Never Do:

  • ❌ Inline form construction in routers
  • json= parameter for Laravel POSTs
  • ❌ Bypass session wrapper
  • ❌ Assume JSON responses
  • ❌ Remove HTML parsing
  • ❌ Change parameter names

After Changes:

# Check violations
grep "\.post(" npid-api-layer/app/routers/*.py | grep -v "session.post"
grep "form_data = {" npid-api-layer/app/routers/*.py
grep "json=" npid-api-layer/app/routers/*.py
grep "BeautifulSoup" npid-api-layer/app/routers/*.py

Reference Files:

  • src/python/npid_api_client.py - Python reference
  • npid-api-layer/app/translators/legacy.py - Pattern reference
  • .claude/skills/npid-api-calls.md - Header requirements
  • .claude/skills/npid-video-submission.md - Video workflow

ENFORCEMENT CHECKLIST

Before submitting ANY code change to npid-api-layer:

  • All POST requests use session.post(), not client.post()
  • No inline form construction in routers
  • No json= parameters in POST requests
  • No BeautifulSoup imports in routers
  • All Laravel interactions go through LegacyTranslator
  • Parameter names match Python client exactly
  • Response parsing handles both HTML and JSON
  • Ran verification commands (section 8)
  • No translator pattern violations detected

If ANY checkbox is unchecked, code is NOT ready for submission.