Files
cpas/AGENTS.md
jason 563c5043c6 docs: add AGENTS.md with coding, compliance, and architecture guidance
Establishes AI agent and developer guidelines covering data integrity rules
(immutable scoring fields, soft-delete contract, audit log append-only),
tier system canonical source, migration patterns, coding standards for both
backend and frontend, schema change checklist, PDF generation notes, and
forward-thinking development constraints.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 16:14:19 -05:00

15 KiB
Raw Blame History

AGENTS.md — CPAS Violation Tracker

Developer and AI agent guidance for working on this codebase. Read this before making changes.


Project Purpose

CPAS (Corrective & Progressive Accountability System) is an internal HR tool for documenting employee violations, managing disciplinary tier escalation via a rolling 90-day point system, and producing auditable PDF records. It is a single-container Docker app deployed on a trusted internal network.

This is a compliance tool. Data integrity, auditability, and reversibility are first-class concerns. Every architectural decision below exists for a reason.


Stack at a Glance

Layer Tech
Frontend React 18 + Vite (SPA, served statically by Express)
Backend Node.js + Express (REST API, server.js)
Database SQLite via better-sqlite3 (synchronous, WAL mode, FK enforcement)
PDF Puppeteer + system Chromium (Alpine-bundled in Docker)
Styling Inline React style objects; client/src/styles/mobile.css for breakpoints only
Deploy Docker multi-stage build (Alpine); single container + volume mount at /data

Repository Layout

cpas/
├── Dockerfile                      # Multi-stage: builder (Node+React) → production (Alpine+Chromium)
├── server.js                       # All API routes + audit helper; single Express entry point
├── db/
│   ├── schema.sql                  # Base table + view definitions (CREATE TABLE IF NOT EXISTS)
│   └── database.js                 # DB connection, WAL/FK pragmas, auto-migrations on startup
├── pdf/
│   ├── generator.js                # Puppeteer launcher; --no-sandbox for Docker
│   └── template.js                 # HTML template builder; loads logo from disk
├── demo/                           # Static stakeholder demo page served at /demo
│   └── index.html                  # Synthetic data, no live API calls; registered before SPA catch-all
├── client/
│   ├── vite.config.js
│   ├── src/
│   │   ├── App.jsx                 # Root component + AppFooter
│   │   ├── main.jsx                # React DOM mount
│   │   ├── data/
│   │   │   ├── violations.js       # Canonical violation type registry (type key → metadata)
│   │   │   └── departments.js      # DEPARTMENTS constant; single source of truth
│   │   ├── hooks/
│   │   │   └── useEmployeeIntelligence.js  # Score + history fetch hook
│   │   ├── components/             # One file per component; no barrel index
│   │   └── styles/
│   │       └── mobile.css          # Media query overrides only; all other styles are inline
└── README.md / README_UNRAID_INSTALL.md

Data Model & Compliance Rules

Tables

Table Purpose
employees id, name, department, supervisor, notes
violations Full incident record; contains immutable scoring fields
violation_resolutions Soft-delete records (resolution type, reason, resolver)
violation_amendments Field-level diff per amendment (old → new, changed_by, timestamp)
audit_log Append-only write action log; never delete from this table
active_cpas_scores VIEW: SUM(points) for negated=0 AND incident_date >= 90 days

Immutable Fields (DO NOT allow amendment of these)

The following fields on violations are locked after submission. They are the basis for tier calculation and PDF accuracy. Never expose them to amendment endpoints:

  • points
  • violation_type
  • violation_name
  • category
  • incident_date
  • prior_active_points (snapshot at insert time)
  • prior_tier_label

Amendable fields (non-scoring): location, details, witness_name, acknowledged_by, acknowledged_date

Soft-Delete Pattern

Violations are never hard-deleted in normal workflow. Use the negated flag + violation_resolutions record. Hard delete is reserved for confirmed data-entry errors and requires explicit user confirmation in the UI.

Prior-Points Snapshot

Every INSERT into violations must compute and store prior_active_points (the employee's current active score before this violation is added). This snapshot ensures PDFs always reflect the accurate historical tier state regardless of subsequent negate/restore actions.

Audit Log

Every write action (employee created/edited/merged, violation logged/amended/negated/restored/deleted) must call the audit() helper in server.js. Never skip audit calls on write routes. The audit log is append-only — no UPDATE or DELETE against audit_log.


CPAS Tier System

These thresholds are the authoritative values. Any feature touching tiers must use them.

Points Tier Label
04 0-1 Elite Standing
59 1 Realignment
1014 2 Administrative Lockdown
1519 3 Verification
2024 4 Risk Mitigation
2529 5 Final Decision
30+ 6 Separation

The canonical tier logic lives in client/src/components/CpasBadge.jsx (TIERS array, getTier(), getNextTier()). Do not duplicate this logic elsewhere — import from CpasBadge.

The 90-day rolling window is computed by the active_cpas_scores view. This view is dropped and recreated in database.js on every startup to ensure it always reflects the correct negated=0 filter.


Violation Type Registry

All violation types are defined in client/src/data/violations.js as violationData. Each entry includes:

{
  name: string,         // Display name
  category: string,     // Grouping for UI display
  minPoints: number,    // Slider minimum
  maxPoints: number,    // Slider maximum (min === max means fixed, no slider)
  chapter: string,      // Policy chapter reference
  fields: string[],     // Which context fields to show ('time', 'minutes', 'amount', 'location', 'description')
  description: string,  // Plain-language definition shown in UI
}

To add a new violation type: add an entry to violationData with a unique camelCase key. Do not add new categories without confirming with the project owner — categories appear in UI groupings.


Coding Standards

Backend (server.js)

  • Use better-sqlite3 synchronous API. No async DB calls. This is intentional — it simplifies route handlers and matches Express's sync error handling.
  • All prepared statements use positional ? parameters. Never interpolate user input into SQL strings.
  • Every POST/PUT/PATCH/DELETE route must:
    1. Validate required inputs and return 400 with a descriptive { error: '...' } body on failure.
    2. Call audit() on success.
    3. Return { error: '...' } (not HTML) on all error paths.
  • Group routes by resource (Employees, Violations, Dashboard, Audit). Match the existing comment banner style: // ── Resource Name ───.
  • Do not add authentication middleware. This runs on a trusted internal network by design.

Frontend (React)

  • Styling: Use inline style objects defined as a const s = { ... } block at the top of each component file. Do not add CSS classes or CSS modules — except for responsive breakpoints which go in mobile.css.
  • Data constants: Import violation types from ../data/violations, departments from ../data/departments, tier logic from ./CpasBadge. Do not hardcode these values in components.
  • Toasts: Use useToast() from ToastProvider for all user-facing feedback. Do not use alert() or console.log for user messages.
  • HTTP: Use axios (already imported in form/modal components). Do not introduce fetch unless there is a compelling reason — keep it consistent.
  • State: Prefer local useState over lifting state unless data is needed by multiple unrelated components. The only global context is ToastProvider.
  • Mobile: Test layout at 768px breakpoint. Use the isMobile media query pattern already in Dashboard.jsx / DashboardMobile.jsx. Add breakpoint rules to mobile.css, not inline styles.
  • Component files: One component per file. Name the file to match the export. No barrel index.js files.

Database Migrations

New columns are added via the auto-migration pattern in database.js. Do not modify schema.sql for columns that already exist in production. Instead:

// Example: adding a new column to violations
const cols = db.prepare('PRAGMA table_info(violations)').all().map(c => c.name);
if (!cols.includes('new_column')) db.exec("ALTER TABLE violations ADD COLUMN new_column TEXT");

Add a comment describing the feature the column enables. schema.sql is only for base tables — use it only for brand-new tables.


Schema Changes: Decision Checklist

Before adding a column or table, answer:

  1. Does it affect scoring? If yes, it must be immutable after insert and included in prior_active_points computation logic.
  2. Does it need audit trail? If it tracks a change to an existing record, add a corresponding entry pattern to violation_amendments or audit_log.
  3. Is it soft-deletable? Prefer negated/flag patterns over hard deletes for anything HR might need to reverse.
  4. Does it appear on PDFs? Update pdf/template.js to reflect it. Test PDF output after schema changes.
  5. Does active_cpas_scores view need updating? If the new column affects point calculations, update the view recreation block in database.js.

PDF Generation

  • PDFs are generated on-demand via GET /api/violations/:id/pdf. No pre-caching.
  • Template is built in pdf/template.js. It receives the full violation + employee record. Logo is loaded from disk at startup and embedded as base64.
  • Puppeteer launches with --no-sandbox --disable-setuid-sandbox (required for Docker; safe in this deployment context).
  • Acknowledgment rendering: if acknowledged_by is set, show name + date in signature block. If not, render blank wet-ink signature lines.
  • After any schema change that adds user-visible fields, update the template to include the new field where appropriate.

Development Workflow

Local Development (without Docker)

# Terminal 1 — backend
npm install
node server.js          # Serves API on :3001 and client/dist statically

# Terminal 2 — frontend (hot reload)
cd client
npm install
npm run dev             # Vite dev server on :5173 (proxy to :3001 configured in vite.config.js)

Build & Deploy

# Build Docker image (compiles React inside container)
docker build -t cpas .

# Run (local)
docker run -d --name cpas -p 3001:3001 -v cpas-data:/data cpas

# Unraid: build → save → transfer → load → run with --pids-limit 2048
# See README_UNRAID_INSTALL.md for full Unraid instructions

Unraid PID limit is critical. Chromium spawns many child processes for PDF generation. Always include --pids-limit 2048 on Unraid containers or PDF generation will fail silently.

Health Check

GET /api/health returns { status: 'ok', timestamp, version }. The version field is populated by the Dockerfile at build time from git commit SHA. In local dev it returns { sha: 'dev' } — this is expected.


Forward-Thinking Development Guidelines

Adding New Features

  • Score-affecting logic belongs in SQL, not JavaScript. The active_cpas_scores view is the single source of truth for point totals. If you need a new score variant (e.g., 30-day window, category-filtered), add a new SQL view — don't compute it in a route handler.
  • New violation fields: Add to schema.sql for fresh installs AND to the migration block in database.js for existing databases. Both are required.
  • Reporting features: Future aggregate queries should join against active_cpas_scores view and audit_log rather than re-implementing point logic. Structure new API endpoints under /api/reports/ namespace.
  • Notifications/alerts: Any future alerting feature (email, Slack) should read from audit_log or query active_cpas_scores — do not add side effects directly into violation insert routes.
  • Authentication: If auth is ever added, implement it as Express middleware applied globally before all /api routes. Do not add per-route auth checks. Session data (user identity) should flow into performed_by fields on audit and amendment records.
  • Multi-tenant / multi-site: The schema is single-tenant. If site isolation is ever needed, add a site_id foreign key to employees and violations as a migration column, then scope all queries with a WHERE site_id = ? clause.

What NOT to Do

  • Do not compute active CPAS scores in JavaScript by summing violations client-side. Always fetch from the active_cpas_scores view.
  • Do not modify prior_active_points after a violation is inserted. It is a historical snapshot, not a live value.
  • Do not add columns to audit_log. It is append-only with a fixed schema.
  • Do not add a framework or ORM. Raw SQL with prepared statements is intentional — it keeps the query behavior explicit and the dependency surface small.
  • Do not add a build step beyond vite build. The backend is plain CommonJS require(); do not transpile it.
  • Do not use alert(), console.log for user messages, or document.querySelector inside React components.

Documentation Standards

Code Comments

  • Comment why, not what. If the reason for a decision is not obvious from the code, explain it.
  • Use the existing banner style for section groupings in server.js:
    // ── Section Name ─────────────────────────────────────────────────────────────
    
  • Mark non-obvious schema columns with inline SQL comments (see schema.sql for examples).
  • When adding a migration block, include a comment naming the feature it enables.

In-App Documentation

The ReadmeModal.jsx component renders an admin reference panel accessible via the ? Docs button. When adding a significant new feature:

  • Add it to the feature map section of the docs modal.
  • Update the tier system table if thresholds change.
  • Move completed roadmap items from the "Proposed" section to the "Completed" section.

README

Update README.md when:

  • A new environment variable is introduced.
  • The Docker run command changes (new volume, port, or flag).
  • A new top-level feature is added that HR administrators need to know about.

Do not add implementation details to README — that belongs in code comments or AGENTS.md.


Constraints & Non-Goals

  • No authentication. This is intentional. The app runs on a trusted LAN. Do not add auth without explicit direction from the project owner.
  • No external dependencies beyond what's in package.json. Avoid introducing new npm packages unless they solve a clearly scoped problem. Prefer using existing stack capabilities.
  • No client-side routing library. Navigation between Violation Form, Dashboard, and modals is handled via App.jsx state (view prop). Do not introduce React Router unless the navigation model meaningfully grows beyond 34 views.
  • No test suite currently. If adding tests, use Vitest for frontend and a lightweight assertion library for backend routes. Do not add a full testing framework without discussion.
  • SQLite only. Do not introduce Postgres, Redis, or other datastores. The single-file DB on a Docker volume is the correct solution for this scale.