Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0ec3d42
docs(events): adrs and context map
danielhe4rt May 17, 2026
a2e0a80
feat(events): module structure (#249)
GabrielFVDev May 19, 2026
779f759
feat(events): rsvp enrollment end-to-end (#275)
BrunaDomingues May 22, 2026
0febd14
feat(events): implement manual check-in (#278)
davicbtoliveira May 23, 2026
f631ea4
feat(events): waitlist and capacity enforcement (#294)
BrunaDomingues Jun 4, 2026
a51f4b7
feat(events): implement numeric code check-in (#297)
davicbtoliveira Jun 4, 2026
8b3d3c4
feat(events): implement qr code check-in for event enrollments (#237…
YuriSouzaDev Jun 4, 2026
c08f504
pint + deps
danielhe4rt Jun 4, 2026
30ef42b
fix(events): resolve phpstan errors in check-in UI
danielhe4rt Jun 4, 2026
d9a7f09
fix(events): correct tenant_id FK to UUID and unblock test suite
danielhe4rt Jun 4, 2026
c2fa725
feat(events): event closure job and admin status override (#312)
davicbtoliveira Jun 7, 2026
98342e9
feat(events): bot check-in integration (#326)
GabrielFVDev Jun 14, 2026
c8a1cc4
chore(events): enum tricks
danielhe4rt Jun 21, 2026
7b9f85b
POC: i18n no admin de eventos (locale switcher + traduções) (#384)
BrunaDomingues Jul 7, 2026
162c4de
feat(events): application enrollment with form builder (#383)
AdryanneKelly Jul 7, 2026
06844b2
Merge remote-tracking branch 'origin/4.x' into feat/events
danielhe4rt Jul 7, 2026
f3acdc2
style(events): apply pint formatting and phpstan type fixes post-merge
danielhe4rt Jul 7, 2026
be200a8
Merge branch '4.x' into feat/events
AdryanneKelly Jul 8, 2026
c45e252
fix(events): key application answers by a stable field id (#396)
AdryanneKelly Jul 9, 2026
3f5bf89
chore(events): sync 4.x into feat/events (#403)
gvieira18 Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .ai/guidelines/domain/02-module-architecture.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,47 @@ public function boot(): void
</code-snippet>
@endverbatim

## Version constraints — mandatory `^1.0.0` style

Every intra-repo `he4rt/*` module dependency (in the root `composer.json` and in any
module's `composer.json`) MUST be declared with the caret style `^1.0.0`. Never use
loose constraints like `>=1`, `*`, `dev-main`, or a truncated `^1.0`.

@verbatim
<code-snippet name="he4rt/* module constraints" lang="json">
{
"require": {
// GOOD — caret with full three-part version:
"he4rt/identity": "^1.0.0",
"he4rt/moderation": "^1.0.0",

// BAD — loose or truncated constraints:
"he4rt/identity": ">=1",
"he4rt/moderation": "^1.0",
"he4rt/economy": "*"
}
}
</code-snippet>
@endverbatim

This keeps every module pinned to a predictable, SemVer-compatible range and avoids
accidental major upgrades. When you add a new module dependency, match this style.

## Dependency rules

- **Domain** modules never import from Presentation or Integration.
- **Integration** modules may depend on Domain (e.g., Identity for user resolution).
- **Presentation** imports from Domain and Integration, never the reverse.
- Check `CONTEXT-MAP.md` for cross-context constraints.

## Registering a new module — mandatory label sync

Every `app-modules/<slug>/` has exactly one `mod:<slug>` triage label, and vice
versa. When you scaffold a **new** module you MUST, in the same change:

1. **Add a row** to the module table in `workflow/triage-labels` (`mod:<slug>` → `<slug>`).
2. **Create the label on GitHub** so the issue tracker can use it:
`gh label create "mod:<slug>" --color "c2e0c6" --description "<short description>"`

Do not leave the guideline table and the live GitHub labels out of sync — an issue
that can't be tagged with its module's label means triage and routing break.
55 changes: 55 additions & 0 deletions .ai/guidelines/domain/06-typed-json-casts.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@php
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
@endphp

# Typed JSON Casts — Ban the Loose Array Cast

**Priority: HIGH.** Every JSON/jsonb column with a known or semi-structured shape MUST be
cast to a typed value object. A loose `'metadata' => 'array'` is an untyped, unvalidated,
refactor-hostile blind spot: PHPStan collapses to `mixed`, malformed payloads become
silent missing keys, and renaming a key is find-and-pray.

The same rule applies beyond casts: **prefer DTOs/VOs over associative arrays and
`stdClass` anywhere a shape is predictable** — command payloads, event properties,
integration Response Objects (Saloon), service returns.

## Banned casts (mechanically enforced)

Banned inside any model's `casts()`/`$casts`: `'array'`, `'json'`, `'object'`,
`'collection'`, `encrypted:array|collection|object`, `AsArrayObject`, `AsCollection`,
`AsEnumArrayObject`, `AsEnumCollection`, `AsEncryptedArrayObject`,
`AsEncryptedCollection`, and `SchemalessAttributes`.

The gate is `tests/Unit/NoLooseArrayCastsTest.php` — it reflects over every concrete
model (`app/Models` + `app-modules/*/src`), reads the real merged `getCasts()`, and fails
on any banned cast not explicitly allowlisted there. The allowlist is the documented
escape hatch for **genuinely polymorphic** JSON (each entry carries a reason); keep it
trending toward empty.

## The pattern

<code-snippet name="Before / after" lang="php">
// BEFORE — blind
'metadata' => 'array',
$model->metadata['last_projects_sync_at'] ?? null; // mixed · magic string

// AFTER — typed VO cast (see AsCredentials in the identity module for a live example)
'metadata' => AsIdentityMetadata::class,
$model->metadata->lastSyncAt(Capability::Projects); // ?CarbonImmutable · verified
</code-snippet>

The VO owns shape, validation and accessors (`fromArray()` / `toArray()` / immutable
`withX()` copies). The cast is only the JSON ↔ VO bridge.

## Rules of thumb

- **Key/format conventions live on ONE owner** (usually the enum:
`$capability->lastSyncedAtKey()`) — never reverse-parse strings to recover a key.
- **The cast generic is a contract for the setter too**: declare
`CastsAttributes<TGet, TGet|array<array-key, mixed>>` so legacy array assignment stays
a reachable, typed branch (`match (true)`).
- `json_decode` returns `array<array-key, mixed>`, never your shape — guard keys or
iterate `EnumClass::cases()` instead of touching arbitrary keys.
- Genuinely polymorphic/freeform JSON → allowlist it **with a reason** and revisit.
- Never introduce `stdClass` in domain code; defensive layers (e.g. the kernel's
`CanonicalJson`) may normalize it, but no domain object produces one.
101 changes: 101 additions & 0 deletions .ai/guidelines/domain/07-enum-filament-contracts.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@php
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
@endphp

# Enums — Implement the Filament Contracts Immediately

**Priority: HIGH.** Every time you create a **new** backed enum, implement the Filament
"enum trick" contracts in the **same change** — never as a follow-up pass. Retrofitting
later means touching every enum (and every call site that assumed a bare `->value`) twice.

Domain enums implementing Filament contracts is the established precedent here (the
moderation enums — `CaseStatus`, `Severity`, `AppealStatus`, `ModerationType`, … — all do
it). The panel leans on these contracts for badges, table/infolist columns, `SelectFilter`
options, select descriptions and state-gated UI.

## Mandatory contracts

| Contract | Method | Always? |
|----------|--------|---------|
| `Filament\Support\Contracts\HasLabel` | `getLabel(): string` | Yes |
| `Filament\Support\Contracts\HasColor` | `getColor()` | Yes |
| `Filament\Support\Contracts\HasDescription` | `getDescription(): ?string` | Yes |
| `Filament\Support\Contracts\HasIcon` | `getIcon()` | Only when an icon is genuinely meaningful |

Every getter is a `match ($this)` over **all** cases — no `default` arm, so adding a case
is a compile-time reminder to fill in every trick.

## HasColor — ordered enums ramp light → red

When the enum encodes an **ordered scale** (access level, seniority, severity, risk, SLA
health), `getColor()` MUST form a monotonic light→red heat ramp: the least/lowest case gets
the lightest color, the most/highest gets `danger` (red). The six semantic strings
(`gray`, `info`, `primary`, `success`, `warning`, `danger`) cover short scales; for a longer
monotonic gradient use the `Filament\Support\Colors\Color` palette.

For an **unordered** enum (e.g. document kinds), pick distinct semantic colors per case —
the ramp rule does not apply.

## The pattern

<code-snippet name="New enum with the full contract set" lang="php">
use Filament\Support\Colors\Color;
use Filament\Support\Contracts\HasColor;
use Filament\Support\Contracts\HasDescription;
use Filament\Support\Contracts\HasIcon;
use Filament\Support\Contracts\HasLabel;
use Filament\Support\Icons\Heroicon;

enum AccessTier: string implements HasColor, HasDescription, HasIcon, HasLabel
{
case None = 'none';
case Analyst = 'analyst';
case Admin = 'admin';

public function getLabel(): string
{
return match ($this) {
self::None => 'No access',
self::Analyst => 'Analyst',
self::Admin => 'Administration',
};
}

// Ordered scale → light (no access) ramps to red (apex authority).
public function getColor(): string|array
{
return match ($this) {
self::None => 'gray',
self::Analyst => Color::Blue,
self::Admin => Color::Red,
};
}

public function getDescription(): ?string
{
return match ($this) {
self::None => 'Notification audience — no panel access',
self::Analyst => 'Day-to-day operator: triage, review, moderation',
self::Admin => 'Final authority: configures the panel, decides cases',
};
}

public function getIcon(): Heroicon
{
return match ($this) {
self::None => Heroicon::OutlinedNoSymbol,
self::Analyst => Heroicon::OutlinedMagnifyingGlass,
self::Admin => Heroicon::OutlinedScale,
};
}
}
</code-snippet>

## Verification

Before marking an enum task done, confirm:

1. The enum `implements HasColor, HasDescription, HasLabel` (and `HasIcon` when meaningful).
2. Every getter `match`es **all** cases with no `default`.
3. Ordered enums ramp light → `danger`; the apex case is red.
4. `{{ $assist->binCommand('pint') }}` and PHPStan pass.
62 changes: 53 additions & 9 deletions .ai/guidelines/workflow/01-issue-tracker.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,58 @@

# Issue tracker: GitHub

Issues and PRDs for this repo live as GitHub issues on `he4rt/he4rt-bot-api`. Use the `gh` CLI for all operations.
Issues and PRDs for this repo live as GitHub issues on `he4rt/heartdevs.com`. Use the
`gh` CLI for all operations — `gh` infers the repo from `git remote -v`, so never
hard-code the `owner/repo`.

## When a skill says "publish to the issue tracker"

Create a real GitHub issue with `gh`, applying the triage/type/module/difficulty
labels from `workflow/triage-labels`, then show the user the created issue URL:

@verbatim
<code-snippet name="Create an issue" lang="bash">
gh issue create \
--title "<type>(<module>): <short description>" \
--body "<markdown body>" \
--label "type:feat,mod:panel-admin,needs-triage"
</code-snippet>
@endverbatim

Use a heredoc for multi-line bodies.

## When a skill says "fetch the relevant ticket"

Read it from GitHub rather than asking the user to paste it. Always include
`--comments` — the resolution usually lives in the thread (reporter clarifications,
the answer to a `needs-info` question, "dupe of #50"), not the opening post:

@verbatim
<code-snippet name="Read / list issues" lang="bash">
gh issue view <number> --comments
gh issue list --state open --label "needs-triage"
</code-snippet>
@endverbatim

## Pull requests as a triage surface

The triage queue is **GitHub Issues** — `/triage` does not pull pull requests; PRs go
through normal code review, not triage.

Because issues and PRs share one number space, a bare `#42` may be either — resolve
with `gh pr view 42` and fall back to `gh issue view 42`.

## Triage labels

The taxonomy in `workflow/triage-labels` maps to **live GitHub labels**. Apply them
with `gh issue edit <number> --add-label "..."`. If a label does not exist yet,
create it first:

@verbatim
<code-snippet name="Create a missing label" lang="bash">
gh label create "mod:<name>" --description "<short description>" --color "c2e0c6"
</code-snippet>
@endverbatim

## Conventions

Expand All @@ -14,13 +65,6 @@
- **Comment on an issue**: `gh issue comment <number> --body "..."`
- **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
- **Close**: `gh issue close <number> --comment "..."`
- Confirm with the user before **bulk** operations or **closing/deleting** issues — those are outward-facing and harder to undo.

Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.

## When a skill says "publish to the issue tracker"

Create a GitHub issue.

## When a skill says "fetch the relevant ticket"

Run `gh issue view <number> --comments`.
51 changes: 51 additions & 0 deletions .ai/guidelines/workflow/04-branch-naming.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
@php
/** @var \Laravel\Boost\Install\GuidelineAssist $assist */
@endphp

# Branch Naming & Git Workflow

Agents MUST create branches using the canonical prefixes below. CI is a **merge
gate** defined in `.github/workflows/continuous-integration.yml`: it runs on a
pull request only when the PR's **base** (target) branch matches one of these
patterns. A branch that becomes a PR target outside this convention gets **no
pipeline**.

## Canonical branch prefixes

| Prefix | Use for | Commit type |
| ------------------------ | ------------------------------------------- | ----------- |
| `feature/<slug>` | New features | `feat` |
| `bugfix/<slug>` | Bug fixes | `fix` |
| `chore/<slug>` | Maintenance, dependencies, tooling, config, CI | `chore` |
| `story/<issue>-<slug>` | Issue-driven work (a GitHub issue is open) | fits the work |

- `<slug>` is lowercase kebab-case, e.g. `feature/public-profile-page`.
- `story/` embeds the issue number, e.g. `story/142-oauth-account-merge`.
- Prefixes map to the `type:` labels documented in the Triage Labels guideline.

## Base / integration branches

Every PR targets one of these bases — the patterns CI is gated on:

| Base pattern | Meaning |
| ----------------------------------------------- | ------------------------------------------------------- |
| `main` | Default line |
| `<major>.x` | Versioned release lines (`1.x`, `2.x`) — Laravel style |
| `feature/**` `bugfix/**` `chore/**` `story/**` | Long-lived integration branches that receive sub-PRs |

Long-lived integration branches use the **same prefixes**. Sub-work is PR'd into
them (e.g. `story/142-oauth-account-merge → feature/identity`); the integration
branch later merges into `main` or a `<major>.x` line.

## Rules for agents

- Branch off the correct base and name it with a canonical prefix. Never push
work directly to `main` or a `<major>.x` line.
- Commit subjects follow conventional commits with the module as scope
(`<type>(<module>): ...`) — see the Title Convention in the Triage Labels
guideline.
- CI runs on PRs only; draft PRs are skipped. Open a non-draft PR — or mark it
ready for review — to trigger the pipeline.
- Keep this table in sync with the `pull_request.branches` filter in
`.github/workflows/continuous-integration.yml`; changing one without the other
drifts the merge gate.
14 changes: 11 additions & 3 deletions .env.testing → .env.testing.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:vCE9NeWv1bkp19JvhTCeMk+qPEC52Vim7shJUljveCg=
APP_NAME=Heartdevs
APP_ENV=testing
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_DOMAIN=localhost
Expand All @@ -12,6 +12,9 @@ APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US

# Faster hashing for the test suite
BCRYPT_ROUNDS=4

DEBUGBAR_ENABLED=false
TELESCOPE_ENABLED=false
PULSE_ENABLED=false
Expand All @@ -28,6 +31,11 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

# --- Test database — per-user. Copied to .env.testing, which is UNTRACKED.
# No host/port/credentials are shipped: the test DB location is
# infra-specific. Point these at YOUR Postgres (docker-compose, Lerd,
# or a custom stack). Lerd users: find the mapped port via the Lerd
# web UI or `lerd services`. Edit freely; this file never shows in git. ---
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
Expand Down
Loading