> ## Documentation Index
> Fetch the complete documentation index at: https://davinci.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog format

# Berkine changelog format

The authoring spec for release entries on the Berkine changelog page.

Take this file into any other repository, hand it to whoever (or whatever) is writing up the
releases there, and what comes back should paste straight into `config/changelog.php` with no
reformatting.

***

## 1. Where the data lives

| Thing | File |
| - | - |
| The entries | `config/changelog.php` → `releases` array |
| The rendering | `resources/views/changelog.blade.php` |
| The route | `routes/web.php` → `Route::view('changelog', 'changelog')->name('changelog')` |
| The slide-open panel | `resources/css/system.css` → `@utility disclosure` |
| Deep links into a closed panel | `resources/js/app.js` → `initDisclosures` |

There is no controller and no database table. A release is an array entry, nothing more. The view
reads `config('changelog.releases')` at render time, so a new entry is live as soon as the config
cache is cleared.

**Never edit the Blade file to add a release.** The only thing that changes on ship day is the config
array.

***

## 2. Entry shape

One array per release. Six keys, always in this order:

```php theme={null}
[
    'date' => '2026-08-21',
    'product' => 'MagicAds',
    'kind' => 'script',
    'version' => '2.1',
    'tag' => 'Breaking',
    'changes' => [
        'breaking' => ['…'],
        'added' => ['…'],
        'improved' => ['…'],
    ],
],
```

**There is no `title` and no `summary`.** An entry is a version, a date and the list of what changed
under it. Do not write prose fields — the view does not read them, so anything you put there is
invisible. A reader scanning for whether a release affects them is reading the bullets, which means
the work goes into the bullets.

The version is the heading, set in the accent colour. It is the first thing read in an entry.

### `date` — required

ISO `YYYY-MM-DD`. Parsed with `Carbon::parse()`.

This is **the only ordering key**. Everything else about position is derived from it, so an entry
dropped in the wrong place in the file still lands in the right year and the right slot.

It must be the real ship date. Three figures in the page masthead are computed from these dates
(last shipped, releases in the last twelve months, products covered), and the page is written so it
cannot claim a number the list below it disagrees with. Fudging a date moves a headline figure.

### `product` — required

The listing the release belongs to, spelled **exactly** as the product record spells it.

The view resolves product names against published products (`App\Support\Catalog::cards()` for
Scripts and Plugins). A name that resolves gets linked to its product page; a name that does not is
set as plain text instead of a dead link. So a typo does not break the page, it just silently drops
the link.

Check the spelling against `database/seeders/ScriptSeeder.php` or `PluginSeeder.php` before you
start, not after. The names there are deliberately unspaced — `MagicAds`, `DavinciAI` — and
`Magic Ads` will render as dead plain text with no warning.

The platform itself is always `'Berkine platform'` — it is not a listing, so it never links.

### `kind` — required

Exactly one of:

| Value | Renders as | Means |
| - | - | - |
| `script` | Script | A standalone application |
| `plugin` | Plugin | An add-on |
| `platform` | Platform | This site: checkout, licences, the buyer area |

Anything else renders as the raw string you typed, which will look wrong. Use these three.

### `version` — required

**The heading of the entry.** With no title to carry it, the version is what the view sets in the
accent colour at heading size, with the product name muted beside it. It is also the entry's own
permalink. Everything else in the entry is set smaller and quieter than this.

As tagged, no `v` prefix — the view adds it. Two conventions in use:

* Products: dotted numeric, `2.1`, `1.9`, `1.0`
* Platform: calendar, `2026.8`, `2026.6`, `2025.9`

`product` + `version` together form the permalink anchor, via
`Str::slug($product.' '.$version)` → `#magicads-2-1`, `#berkine-platform-2026-8`. **The pair must be
unique across the whole log** or two entries share an anchor and support links point at the wrong
release. A permalink still works when its product panel is collapsed: `initDisclosures` in app.js
opens the ancestors and scrolls to the entry.

### `tag` — required, may be null

The only badge on the page. Three permitted values:

* `'Breaking'`
* `'Security'`
* `null`

Always write the key, even when it is `null`. Do not omit it.

The tag is not decorative, it is derived:

* `changes.breaking` present → `'Breaking'`
* `changes.security` present and no breaking changes → `'Security'`
* otherwise → `null`

Most releases are `null`. A badge on an ordinary release makes the badge meaningless.

### `changes` — required

An array keyed by kind of change. **Five permitted keys and no others:**

```
added  improved  fixed  security  breaking
```

Any other key is silently ignored by the view — it will not appear on the page and nothing will
warn you.

Include only the groups that apply. Two or three groups per entry is typical; five is almost never
honest.

Each group is a flat list of strings.

***

## 3. Ordering rules

The page nests three levels: **product → year → release.** All three are derived, none of them are
declared. You write a flat list of entries and the view does the grouping.

### Between products

Each product becomes a collapsible panel — a native `<details>` — headed by its name, its release
count, and its latest version and date. The panels are ordered by each product's own most recent
release, newest first, and the leading one opens by default.

Which means: `product` is not just a label, it is the grouping key. A misspelling does not merely
drop a link, it splits one product into two panels. Check it first.

Year anchors are scoped to the product (`#year-magicads-2026`), so two products shipping in the same
year do not collide.

### Between entries

Newest first, by `date`. The file follows that convention for readability, but the view sorts
regardless, then groups by year within each product and sorts the years descending.

### Between change groups

Fixed by the view, not by your array:

1. **Breaking**
2. **Security**
3. **Added**
4. **Improved**
5. **Fixed**

A breaking change or a security fix has to be the first thing seen in an entry, whichever key the
author happened to type first. Write them in this order anyway so the source reads like the page.

Breaking and Security also render in the accent colour; the other three are muted.

### Within a group

Preserved exactly as authored. Put the item with the largest consequence first — the thing a reader
has to act on before the thing that is merely nice.

***

## 4. Writing the bullets

The bullets are the entry. There is no summary above them to set up the release or explain why it
mattered, so a bullet has to stand on its own: what changed, and enough of why to be worth reading.
A bullet that only names a feature — "Added the Image Editor plugin." — leaves nothing on the page.

Volume, per the existing log:

| Group | Typical count |
| - | - |
| `breaking` | 1–2 |
| `security` | 2–3 |
| `added` | 2–3 |
| `improved` | 1–2 |
| `fixed` | 1–2 |

Rules:

* One sentence per bullet, ending in a full stop. Occasionally two short ones.
* 8–25 words. Long enough to be specific, short enough to scan.
* No leading dash, bullet character or label — the view draws the mark.
* Start with the thing, not with "We". "Hard caps per tenant, per seat and per feature, enforced at
  dequeue." Not "We added hard caps."
* Name the mechanism, not the adjective. "enforced before the request leaves the queue" beats
  "reliable enforcement".
* Carry the "so that" where it is not obvious. "Credit cost calculation in the UGC plugin reworked,
  so a generation is charged for what it consumed." The clause after the comma is the part that used
  to live in the summary.
* Numbers where you have them: "from roughly 300ms to under 40ms", "a 400-case set from about 12
  minutes to under 3", "around 60 per cent". Hedge the number ("roughly", "about") rather than
  inflate it.
* `fixed` bullets describe **the bug**, in the past tense, not the repair: "Documents deleted at
  source stayed searchable until the next full reindex." The fix is implied by it being in the log.
* `breaking` bullets must state the action required, and point at the upgrade guide or migration
  command if there is one: "The webhook endpoint for crypto is now /payments/helio/webhook.
  Subscribe it to REGULAR\_TRANSACTION in the Helio dashboard."
* `security` bullets describe the coverage gained, never the exploit path.

***

## 5. House style

* **British English.** licence, behaviour, prioritised, sanitises, diarisation.
* **"per cent"**, two words, spelled out. Not "%".
* Units closed up: `40ms`, `200ms`, `4x`.
* Numbers under ten spelled out in prose (`twelve months`, `three gateways`); figures where they are
  measurements or versions.
* No exclamation marks, no em dashes as decoration, no marketing superlatives. Show the fact.
* Product and feature names as they are actually cased: `Livewire 4`, `Laravel 13`, `PHP 8.4`,
  `Stripe`, `Helio`.
* Code-ish identifiers stay bare, no backticks — the view renders plain text, so backticks would
  print literally. Table and column names are fine as `agent_runs` written plainly:
  `agent_runs and agent_steps`.

### PHP string escaping

All strings are single-quoted PHP. Two things to watch:

* ASCII apostrophe must be escaped: `'the caller\'s own permissions'`
* A typographic apostrophe needs no escape: `'the storefront’s currency'`

Both appear in the existing file. Either is acceptable; be consistent within an entry.

None of these strings pass through `__()`. They render exactly as written, in English, on every
locale. Only the labels around them are translated.

***

## 6. Formatting

Inside `config/changelog.php`:

* Entry array at **8 spaces**
* Keys at **12 spaces**
* Group keys at **16 spaces**
* Bullets at **20 spaces**
* Trailing comma on every element, including the last
* One blank line between entries

***

## 7. Template

```php theme={null}
        [
            'date' => 'YYYY-MM-DD',
            'product' => 'ExactProductName',
            'kind' => 'script',
            'version' => '1.0',
            'tag' => null,
            'changes' => [
                'breaking' => [
                    'What changed, and the action the reader has to take.',
                ],
                'security' => [
                    'The coverage gained, not the exploit.',
                ],
                'added' => [
                    'The thing, stated as a thing, with the mechanism named.',
                    'A second thing, if there genuinely is one.',
                ],
                'improved' => [
                    'What got better, with a measured number where one exists.',
                ],
                'fixed' => [
                    'The bug as it behaved, in the past tense.',
                ],
            ],
        ],
```

Delete the groups that do not apply. Do not leave empty arrays — the view skips them, but they are
noise in the source.

***

## 8. Worked example

```php theme={null}
        [
            'date' => '2026-08-24',
            'product' => 'MagicAds',
            'kind' => 'script',
            'version' => '2.0',
            'tag' => 'Breaking',
            'changes' => [
                'breaking' => [
                    'The marketplace endpoint is now marketplace.berkine.dev. Update API_URL in app/Http/Controllers/Admin/ExtensionController.php first, or the Update page returns a 500.',
                    'Visit /app/admin/update/now after the package installs, which migrates and seeds the model tables for this version.',
                ],
                'added' => [
                    'Image Editor plugin, for reworking a generated image without generating it again.',
                    'Ad Copy Editor, so a variant can be edited in place before it ships.',
                    'OpenAI GPT 5.6 Sol, Terra and Luna, Anthropic Claude Opus 5 and Sonnet 5, Google Gemini 3.7 Flash, and Bytedance Seedance 2.5.',
                ],
                'improved' => [
                    'SaaS Business plugin, Avatar Studio and UGC Factory updated for the new model list.',
                ],
                'fixed' => [
                    'The default language setting did not always take effect on a new session.',
                ],
            ],
        ],
```

That renders as a heading of **v2.0** in the accent, *MagicAds* muted beside it, `24 Aug 2026` and a
`Breaking` badge above, then the four groups in the fixed order.

***

## 9. Checklist before committing

* [ ] `date` is ISO, real, and the entry sits newest-first in the file
* [ ] `product` matches the published product name character for character, checked against the seeder
* [ ] `kind` is one of `script`, `plugin`, `platform`
* [ ] `product` + `version` is unique across the whole log
* [ ] `tag` key is present, and agrees with the presence of `breaking` / `security`
* [ ] Six keys only — no `title`, no `summary`, no anything else
* [ ] `changes` uses only the five permitted keys
* [ ] Every bullet is a full sentence ending in a full stop, with no leading dash
* [ ] Every `breaking` bullet names the action required
* [ ] ASCII apostrophes escaped as `\'`
* [ ] Indentation and trailing commas match section 6
* [ ] `php artisan config:clear` after deploying, or the new entry stays invisible
