# Brand fork kit

This repository (`petav3-hk`) is **not a separate product**. It is `petav3` — the
shared codebase — plus a thin brand layer. Upstream owns every feature; this repo
owns only a name, a logo and a colour ramp.

Keeping it that way is the whole job. The moment this fork starts holding real
feature code, every future sync becomes a merge argument instead of a command.

---

## Syncing with upstream

```bash
bash scripts/brand/sync-upstream.sh
```

That is the entire routine. It fetches `petav3`, merges it onto a
`sync/upstream-<date>` branch, re-applies the brand layer to everything upstream
brought in, and prints a report. Then build, test, push:

```bash
bash scripts/live-build.sh --force
npx vitest run
git commit && git push -u origin sync/upstream-<date>
```

**Never `cherry-pick` from upstream.** A cherry-pick copies a commit under a new
hash, so `git` can no longer tell you what this fork is actually missing —
`git rev-list --left-right --count HEAD...upstream/master` starts lying, and the
next person cannot tell a synced commit from an un-synced one. Merge, always.

### What the script decides, and what it refuses to decide

For each conflicted file it rebuilds *what this fork's version would look like if
the only thing the fork had ever done to it was rebrand it*.

* Matches the fork's real version → the fork has no genuine change here, so it
  takes upstream's file and rebrands it. Silent, safe, logged in the report.
* Does not match → the fork really edited that file. The script stops and names
  it. Resolve by hand.

So the report's `NEEDS A HUMAN` list is also a health check: **it should
normally be empty.** A file that keeps appearing there is feature code that has
drifted into the fork, and it belongs upstream instead.

---

## Colour and logo: nothing is "ignored", everything is re-derived

There is no ignore list, and there could not be one: git has no way to say
"keep my version of these 300 files" that does not also keep them frozen at
whatever upstream looked like when you forked. Instead the sweep RE-DERIVES the
brand from upstream's own code after every merge:

| Upstream writes | The sweep produces | Where the value comes from |
|---|---|---|
| `bg-blue-600`, `border-t-indigo-500` | `bg-brand-600`, `border-t-brand-500` | Tailwind token, so the ramp below decides the colour |
| `#2563eb` in a chart / mapbox marker | `#3f4650` | `RAMP_600` |
| `--color-brand-600: #2563eb` in `app.css` | `--color-brand-600: #3f4650` | `RAMP_600` — **the palette file is swept too, not hand-kept** |
| `/images/logo/logo.png` | `/images/logo/red-text-non-bg.png` | `LOGO_SUBS` |
| `/images/logo/icon.png` | `/images/logo/starcity-mark.png` | `LOGO_SUBS` — square mark, not the wordmark |
| `PropertyLab` | `Starcity Global` | `BRAND_NAME` |

So `resources/css/app.css` and `AppLogo.vue` are **not** hand-maintained. They
are byte-for-byte what `brandize.sh` produces from upstream's version, which is
exactly what lets `sync-upstream.sh` resolve them automatically instead of
asking. Keep it that way: if you hand-edit the palette, put the new hex in
`brand.conf` too, or the next sync will hand it back to you as a conflict.

Only the `.env` (domains, mail, keys) and the logo image files themselves are
genuinely this fork's own.

### The part that is NOT automatic

About 40 files still differ from `brandize.sh`'s output, and the sync report
names them. Roughly 32 are the same thing: **the Starcity logo has different
proportions from upstream's**, so `h-9` became `h-20`, `width="160"` became
`width="100"`. That is a design decision, not a substitution — no rule can
derive it. The other 8 carry real Hong Kong content (contact details, landing
copy).

When upstream touches one of those files, the sync stops and asks. That is
correct: it is the only kind of file where a human genuinely knows something the
script does not. If a file lands there and it turns out to be neither of these
two things, it is drift — fold it back into `brand.conf` or push it upstream.

Everything else should be identical to upstream. If you need a behaviour change,
make it **upstream in `petav3`, behind a config flag** — the HK-only HSBC
valuation work already lives there, which is the pattern to copy.

---

## Starting a THIRD brand

1. Fork `petav3` on GitHub (do **not** fork this repo — start from upstream, so
   the new brand inherits no HK decisions).
2. Copy `scripts/brand/` across.
3. Edit `scripts/brand/brand.conf` — name, upper-case name, and the seven ramp
   colours.
4. Put the same seven colours into the `@theme` block of `resources/css/app.css`.
   **They must match `brand.conf`.** Charts, mapbox markers and canvas drawings
   cannot read a CSS class, so `brandize.sh` rewrites their hard-coded hex from
   `brand.conf`; if the two lists disagree, the charts quietly drift a different
   colour from the rest of the page.
5. Drop the logo into `public/images/logo/` and point `resources/js/Components/AppLogo.vue` at it.
6. `bash scripts/brand/brandize.sh` — one sweep, whole repo.
7. Build, eyeball, commit.

---

## Rules the sweep follows (and why)

* **Lower-case `propertylab` is never renamed.** It is a config key, an AI
  persona id, a stored column value and a domain — `propertylab_ai`,
  `app.propertylab.com.my`. Renaming it breaks lookups against rows that are
  already in the database.
* **`PropertyLab` glued to more letters is never renamed.** Those are class
  names: `PropertyLabEvaluationController`, `EvaluatePropertyLabGeneration`.
  A blind replace turns them into `Starcity GlobalEvaluationController` and the
  app fatals. The lookarounds in `brandize.sh` are what prevent that.
* **`resources/lang/*.json` is swept key AND value.** In Laravel's JSON
  translations the key IS the English source string, so renaming the brand in a
  `.vue` file without renaming the key silently drops the zh_CN / ms translation
  for that line and shows English instead.
* **`resources/css/app.css` is excluded.** It defines the ramp the sweep uses;
  sweeping it would be circular.
* **The vendor's own names are never renamed.** A white-label client BUYS its
  AI from PropertyLab, and the thing it is billed for has to carry PropertyLab's
  name, not the client's — Starcity's Setting tab read "Starcity Global API"
  until this rule existed (2026-09-06). Two escapes, because they cover two
  different shapes:
    * `KEEP_PHRASES` in `brand.conf` — whole phrases spared everywhere they
      appear (`PropertyLab API`, the service). The bare name is still renamed
      around them.
    * a trailing `brand:keep` comment — spares one LINE. Upstream puts it on the
      service / model-label constants and the "contact PropertyLab" banner, where
      the phrase alone is not enough: `PropertyLab AI` is the model label on
      those lines but the chat persona everywhere else, and the persona IS the
      client's to rename.
  The Hub also serves both names on `/me` and the client prefers them at
  runtime; the kept constants are what the page shows before the Hub is first
  reached, and what the sweep would otherwise hand the client to rename.

---

The full sync runbook — decisions already taken, pitfalls, the test baseline — is [`/docs/modules_handbook/hk/readMe.md`](/docs/modules_handbook/hk/readMe.md).
