# Kualifi — Project Brief

*Last updated: 8 Sep 2026. Self-contained context for anyone (human or AI) joining the project.*

## What it is

**Kualifi** (kualifi.com) is a multi-tenant SaaS by **AI4Good** that prepares Malaysian SMEs
for independent assessment against **Malaysia's national ESG standard** (internally SIRIM
55:2023 — see the interim naming rule below). Tagline: **"Making ESG readiness achievable."**

The pitch: SMEs get ESG questionnaires from purchasers and banks, have evidence scattered
across inboxes, and can't afford consultants. Kualifi turns the standard into guided
questions, practical actions, evidence storage, scheduled duties and a printable
**Assessment Pack** — self-service software with optional expert advisory.

It grew out of an ESG module originally built inside Dale Carnegie Malaysia's VoC portal
(dale.my); that portal is a separate codebase and must never be touched by Kualifi work.

## Brand (authority: `KualifiBrand/Corrected-Assets/`, guidelines v1.3 — in-repo)

- Colours: navy `#0B1739`, cobalt `#3157F6` (actions/links), lime `#B7F34A` (accents on
  navy ONLY), teal `#0C9B8E` (positive status), warm canvas `#F7F5EF`. Fonts: Plus
  Jakarta Sans (display) + Inter (body). Pill buttons, 16px radius.
- Voice: plain, empathetic, honest. British English, no Oxford comma, **unspaced em
  dashes** (word—word). Data-backed claims only. Outcome labels **Foundation / Building /
  Progressing / Assessment-ready**; "Assessment-ready" always paired with
  "Kualifi methodology v1 · <date>".
- **INTERIM RULE (in force until SIRIM grants written permission): no user-visible copy,
  email or AI-generated document may name SIRIM.** Say "Malaysia's national ESG
  standard". Clause numbers (4.1, 8.3…) are fine. It's an *Assessment Pack*, users
  *prepare for independent assessment*; the approved no-certification disclaimer appears
  on landing, footer and pack. Internal identifiers (`standards.code='SIRIM55'`) exempt.
- The one example company in ALL user-facing guidance: **Roti Harum Enterprise, a
  12-person bakery in Shah Alam**.

## Stack & architecture

- **Plain PHP 8 + PDO + MySQL on cPanel shared hosting.** No framework, no Composer, no
  build step, no local PHP in dev (verification = string-aware syntax scanner + reading
  + adversarial multi-agent review passes).
- Layout: `public/` is the only web-served folder (docroot of kualifi.com); `src/`,
  `config/` (config.php — live credentials on the server, git skip-worktree'd locally),
  `sql/`, `bin/` (cron CLIs), `uploads/esg/{company_id}` (evidence, outside webroot)
  sit above it. Deny-all `.htaccess` in every non-public folder as backstop.
- **Multi-tenancy: single DB; every tenant table carries `company_id`; every query
  filters it via `Tenant::id()`. No page may ever accept a company_id from user input.**
- **Template-copy pattern**: global `std_*` seed tables → per-company copies at
  activation (`Tenant::activateStandardContent()`), now carrying **stable codes**
  (below). Multi-standard schema (`standard_id`), SIRIM55 only in v1.
- Key src classes: `Db`, `Auth` (tenant + padmin surfaces; audited impersonation),
  `Tenant`, `Esg`, `EsgSchedule` (duty-date engine), `Assessment` (scoring),
  `QuestionBank` (bank versioning), `Claude` (AI client), `Mailer` (SMTP2GO + mail()
  fallback + sent-mail evidence), `Guide`, `Journey`, `Suggestions`.

## Data-integrity regime (the v2-migration groundwork — all live)

v1's modules will eventually be rebuilt on a separate v2 platform (append-only,
measurements with periods/provenance); customers migrate current-state-only, v1 stays
readable as history. `docs/V1-MIGRATION-ANSWERS.md` answers the schema questionnaire.
What shipped to make that migration a script rather than an archaeology dig:

- **Soft delete (KF-260825-0001, migrate_04)**: the eight tables behind × buttons
  (laws, aspects, stakeholders, roles, actions, logbook, objectives, documents) carry
  `deleted_at`/`deleted_by`; deleting flags, never destroys; **evidence bytes and
  esg_files rows survive their parent's deletion**; every list/count/report query
  excludes flagged rows (documented exceptions in DECISIONS). `deleted_by` is v1's only
  edit attribution. Register `created_at` added, historical rows deliberately NULL.
- **Stable codes (KF-260825-0002, migrate_05)**: laws = official act numbers
  (`MY-ACT-127`…), aspects = `E-CONS-WATER`-style slugs, stakeholders = `SH-*`;
  `std_code` on tenant copies backfilled by exact label match; custom rows NULL.
  A code is an identity, never renamed or reused. `sql/std_labels.lock.json` locks
  code→label (laws/aspects/stakeholders **and std_requirements clause text**);
  **`bin/check_std_integrity.php` must print INTACT** — run it after any deploy touching
  reference content. Label amendments = seed + manifest in the same commit.
- **Question-bank versioning (KF-260825-0003, migrate_06)**: weights are immutable
  (blocked in the editor, checker-enforced); every real bank change (text/active/sort/
  add/band thresholds) appends a full JSON snapshot to append-only
  `question_bank_versions` and bumps the pointer; **every attempt stamps
  `assessments.bank_version`** at completion. Pre-versioning attempts stay NULL. The
  user-facing methodology stamp string is unchanged.
- **Demo data**: truly purged from production (bin/purge_demo_companies.php,
  clone-rehearsed). The demo fixture now lives DISARMED at
  `dev-fixtures/demo-companies.NEVER-IMPORT.sql` (KF-20260908-06): importing
  it untouched inserts nothing — arming requires editing `@armed := 1`, and
  even armed it refuses any database holding a real (non-.example) tenant.

## The user journey (unchanged in substance)

Public funnel (two-step Readiness Assessment, questions first; guest results claimable
at signup) → registration w/ state (drives holidays + Fri–Sat weekends for
KDH/KTN/TRG) → 8-step wizard (scope → stakeholders → laws → risks L×S → roles w/
one-click proposal → AI-drafted documents, Owner approves → objectives w/ curated
suggestions → activation arms the duty calendar) → dashboard ("Your path" journey,
duties, 19-requirement checklist) → registers/system pages (all with plain-English
guide boxes) → printable Assessment Pack. Padmin console: tenants (AI quota + per-tenant
US$ cost), leads, assessment editor (bank version shown; weights read-only), holidays,
audit log.

## Email (KF-20260908-01 + -02, live)

- **Transport: SMTP2GO HTTP API** (raw cURL) with **fallback to PHP mail()** on any
  provider failure; the delivering transport is recorded. API key: one line at
  `/home/dalecarn/kualifi-smtp2go.key` (above public_html, chmod 600); an empty/missing
  key disables the provider cleanly. Tracking off. SPF includes `spf.smtp2go.com` AND
  keeps the host IP (the fallback must keep passing); SMTP2GO's 3 CNAMEs added; DKIM +
  DMARC (`p=quarantine`) unchanged.
- **Evidence: every message leaves a `sent_email` row** (migrate_07): company,
  recipient, type (verification/invite/reset/reminder/lead), subject, transport,
  provider message id, accepted flag, sent_at. **No bodies stored.** FK SET NULL — the
  record outlives the company. `reminded_at` chase logic untouched.
- Messages: verification, invite, reset (48h single-use hashed tokens), daily duty
  digest (`bin/reminders.php` cron, 14-day window, 7-day re-chase), lead notifications.

## Security posture (KF-20260908-03 + prior, live)

- **HTTPS forced**: `public/.htaccess` 301s all plain HTTP before PHP runs (none
  existed before — the login form was reachable over cleartext); HSTS 1 year (no
  includeSubDomains — SMTP2GO CNAMEs are external; no preload); session cookie Secure
  flag unconditional; one-shot `session_regenerate_id` upgraded pre-deploy cookies.
- Sessions: PHP native, cookie `kualifi_sess`, HttpOnly + SameSite=Lax + Secure,
  browser-session lifetime; no remember-me. Passwords: bcrypt cost 10 via
  `PASSWORD_DEFAULT` (rehash-on-login upgrade path) — portable hashes.
- Git history audited: no live credential ever committed (config.php committed versions
  are placeholders only). CSRF on every POST; `h()` on output; no-enumeration token
  flows; uploads whitelisted, outside webroot, tenant-gated via `esg_file.php`.
- Scanner-safe verify links (confirm-on-POST — corporate SafeLinks burns GET tokens).

## Ops & working conventions

- **Deploy runbook: `docs/DEPLOY.md`** (§6b migrations 01–07 in order, §6c SMTP2GO,
  §6d HTTPS). Golden rule: **import migrations BEFORE uploading code**. Deploys are
  manual (Dreamweaver sync / cPanel File Manager); CLI scripts run via one-shot cron
  (host `php` is the CGI binary — bin scripts carry shims). Daily crons: reminders.
  `bin/check_std_integrity.php` after any reference-content deploy.
- **Work items**: ids `KF-YYYYMMDD-NN` are assigned BY THE PRODUCT OWNER only — never
  self-generated. One branch per item named by its id; id in every commit message; stop
  for owner review between items; state the target database and wait for confirmation
  before any DB write; nothing cross-platform (shared with v2) is decided unilaterally.
- Adversarial multi-agent review (independent finders + refutation verifiers) before
  every substantive commit — it catches real defects nearly every time.
- Dreamweaver hazards: cloak `.git` and `_notes` (it has written dwsync.xml into
  `.git/refs/` and pulled server `error_log` files into the tree — `error_log` is now
  gitignored; `_diag.php`/`first_admin.php` were removed from the repo so syncs cannot
  resurrect them).
- Decision record: `docs/DECISIONS.md` (change decisions there, not by drive-by edits).

## Open / blocked (do not resolve silently)

1. **Blocked on v2 vocabularies the owner is fetching — invent NO placeholders**:
   evidence capture date, evidence review level, reporting period, non-applicability
   reason codes for laws/aspects.
2. **Undecided**: Assessment Pack storage (leave `esg_report.php` alone); tracking of
   abandoned assessment attempts (leave `Assessment::saveAttempt()` alone). Note:
   `std_requirements` is seeded-only with no version marker — if Pack storage lands, it
   needs the question-bank treatment first.
3. **SIRIM licensing/permission** — gates PUBLIC launch and lifts the interim naming
   rule; private pilot fine meanwhile. "Kualifi" trademark clearance pending.
4. **Payments** — plans/subscriptions/payments tables dormant; every company on free.
5. Landing nav omits Resources/Pricing until those pages exist; real testimonials to
   replace the About metrics strip when customers exist.
