feat(fase-17): CHANGELOG.md backfill + Keep-a-Changelog linter
Adds repo-root CHANGELOG.md in Keep-a-Changelog format with a single [v0.0.0-beta] section that catalogues every shipped feature across Fases 0–16 (58 bullets, [new]/[fix]/[tweak] prefixes, grouped by area). The companion linter at scripts/check-changelog.mjs enforces (a) the Keep-a-Changelog header, (b) the `## [vX.Y.Z] — YYYY-MM-DD` heading shape on every release, and (c) the three-prefix rule on every bullet. Wired into `just check` so a malformed CHANGELOG fails the check gate without blocking commit hooks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
96
CHANGELOG.md
Normal file
96
CHANGELOG.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
Each bullet is prefixed with `[new]` (added capability), `[fix]` (resolved
|
||||||
|
defect) or `[tweak]` (behaviour or implementation change with no new
|
||||||
|
surface). Areas group related entries; section bullets ending in `:` are
|
||||||
|
labels for the area, not log entries.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [v0.0.0-beta] — 2026-05-19 — MVP + MVP2 (Fases 0–16)
|
||||||
|
|
||||||
|
Single rollup for everything shipped before tagging started. Covers the
|
||||||
|
entire MVP cycle (Fases 0–7) plus the prod auth hardening pass (Fase 8)
|
||||||
|
and the eight MVP2 fases (9–16). Reads as a feature catalogue, not a
|
||||||
|
day-by-day diff — the per-fase docs in `docs/history/` and the per-fase
|
||||||
|
plans in `plan/` are the source of record.
|
||||||
|
|
||||||
|
- Auth + identity:
|
||||||
|
- [new] Keycloak → GoTrue → Supabase OIDC integration (PKCE), with `auth.users` pre-seed for the 5 dev users (Fase 1).
|
||||||
|
- [new] Self-registration via Keycloak; invitations are link-only (no email), generated on `/collective/manage` (Fase 1).
|
||||||
|
- [new] Multi-collective per user with sidebar switcher; active collective persisted to `localStorage` (Fase 1).
|
||||||
|
- [new] Atomic `create_collective(name, emoji)` RPC so onboarding works in a single round-trip without tripping `is_admin` / `is_member` policies (migration 012, Fase 7).
|
||||||
|
- [new] Public `/logged-out` route + RP-initiated Keycloak end-session on logout, so the SSO cookie is actually cleared and the next login re-prompts (Fase 8).
|
||||||
|
- [new] Account hard-delete (`delete_account()` RPC) with sole-admin auto-promote and a Danger-zone modal (migration 021, Fase 10.5).
|
||||||
|
- [fix] `auth.users.role`/`aud` default-fill trigger so PostgREST routing survives the Keycloak provisioning path; INSERT + UPDATE both covered to defeat GoTrue's pop-ORM re-clobber (migrations 013–014, Fases 7–8).
|
||||||
|
- [fix] `isLoggingOut` guard in `(app)/+layout.svelte` to suppress the auth-gate `$effect` during the logout navigation, closing a "PKCE code verifier not found in storage" race on prod (Fase 8).
|
||||||
|
- [fix] `pendingInvitationToken` restored from `sessionStorage` after login so the user lands on `/invitation/<token>` instead of `/onboarding` or `/lists` (Fase 7).
|
||||||
|
|
||||||
|
- Shopping lists + items:
|
||||||
|
- [new] Lists overview (featured + grid + completed + trash) and detail view with checkbox toggle, qty stepper, inline edit, drag-reorder (Fase 2a).
|
||||||
|
- [new] Frequency-driven suggestion chips on the add-item form, backed by the `item_frequency` table + trigger (Fase 2a; migration 006).
|
||||||
|
- [new] Realtime "Modo Compra" full-screen session view with 56 px toggles, flip animation, complete-list → redirect; postgres_changes subscription per list with echo-deduplication (Fase 2b; migration 007).
|
||||||
|
- [new] Offline-first sync queue (`SyncQueue` IDB-backed, FIFO flush, PK-conflict-as-success) with `online`-event auto-flush; offline/syncing banner mounted in detail + session views (Fase 2b).
|
||||||
|
- [new] Selection mode with bulk delete / move / mark-checked, long-press entry on mobile + desktop toggle button (Fase 5.11).
|
||||||
|
- [new] Reset-from-detail button on completed lists; "Crear nuevo colectivo" entry in sidebar + drawer (Fase 10).
|
||||||
|
- [new] Trash drawer restore + hard-delete via `restore_list()` / `hard_delete_list()` RPCs (migration 020, Fase 10.4).
|
||||||
|
- [new] Collective-scoped item tags (chips + picker + filter bar + realtime resort) backed by `item_tags` + `shopping_item_tags` (migration 022, Fase 11).
|
||||||
|
- [new] Common-items management on `/collective/manage` — 3-way Hide / Normal / Boost segmented control writing `weight` via admin-only RPCs; suggestion strip reorders by weight + use_count + last_used_at (migration 026, Fase 15).
|
||||||
|
- [new] Suggestion dropdown excludes names already on the active list (`excludeNames` capped at 200 to respect Kong URI limits) (Fase 15).
|
||||||
|
- [tweak] Item row redesigned to `drag-handle | name-button | qty stepper` with swipe-to-(un)check; double-tap opens an edit overlay; `unit` column dropped because it became unreachable (migration 011, Fase 5.10).
|
||||||
|
- [tweak] Add-form moved inline as the last child of the unchecked section; sticky bottom bars retired (Fase 5.14).
|
||||||
|
- [tweak] List rename inline at the top of the detail view with 500 ms debounced autosave; "New list" button moved to the masthead (Fase 5.8).
|
||||||
|
|
||||||
|
- Tasks + notes:
|
||||||
|
- [new] Task lists + tasks with completion-consistency CHECK (`is_completed ⇔ completed_by ⇔ completed_at`) and drag-reorder (migration 008, Fase 3).
|
||||||
|
- [new] Notes with 8-colour palette, pinned + archived (mutually exclusive), 7-day trash window enforced via RLS + `pg_cron` purge; editor with 500 ms debounced autosave (migration 009, Fase 3).
|
||||||
|
- [new] Sync-conflicts review route at `/settings/sync-conflicts`, paginated, side-by-side local vs remote payloads, per-device dismissal via `localStorage` (Fase 14.2).
|
||||||
|
|
||||||
|
- Search:
|
||||||
|
- [new] Cross-section search via `search_in_collective()` SECURITY INVOKER RPC over generated `tsvector` columns on items/tasks/notes; debounced input, filter chips, deep-link per result type (migration 010, Fase 4).
|
||||||
|
|
||||||
|
- Mobile UX + accessibility:
|
||||||
|
- [new] Mobile shell — top bar with hamburger + collective name + avatar, slide-in drawer, bottom tab bar with safe-area inset, sticky contextual headers on detail routes (Fase 5).
|
||||||
|
- [new] `UndoToast` for destructive actions (4 s TTL, auto-commit, mounted globally), with `scheduleUndoable()` queue (Fase 5).
|
||||||
|
- [new] Spinner component (`sm/md/lg`, `currentColor`, `motion-reduce:animate-none`, sr-only label) integrated at 7 loading sites (Fase 16).
|
||||||
|
- [tweak] Desktop reading width capped at `max-w-2xl` for content detail routes; mobile and `/lists/[id]/session` excluded (Fase 9.2).
|
||||||
|
|
||||||
|
- PWA + offline:
|
||||||
|
- [new] Installable PWA — Workbox precaching via `@vite-pwa/sveltekit`, manifest, iOS meta tags + apple-touch icons, placeholder slate-and-"C" icons (Fase 6.2).
|
||||||
|
- [new] Service-worker auto-registration via `useRegisterSW` + 15 min `reg.update` poll; `UpdateToast` with "Reload" action when a new build is available; "Ready to use offline" auto-dismiss toast (Fase 14.1).
|
||||||
|
- [new] `OfflineChip` in `MobileTopBar` + `DesktopSidebar` with a pending-ops badge when the queue is non-empty (Fase 14.2).
|
||||||
|
- [new] Build-time version constants (`__APP_VERSION__` / `__APP_COMMIT__` / `__BUILD_TIME__`) exposed via `$lib/version`; "Acerca de" chip on `/settings`; `GIT_SHA` baked through the Dockerfile build arg by the deploy script (Fase 14.3).
|
||||||
|
- [tweak] PWA strategy on prod switched to `generateSW` because `@vite-pwa/sveltekit` 0.6.8 hardcodes the source SW filename under `injectManifest` (Fase 6 → revisit in Fase 14).
|
||||||
|
- [fix] `<link rel="manifest">` added to `app.html` so Android Chrome surfaces the install prompt (Fase 14 follow-up).
|
||||||
|
|
||||||
|
- Collective management:
|
||||||
|
- [new] Rename collective + change emoji (CU-H07) on `/collective/manage` (Fase 10.2).
|
||||||
|
- [new] Leave-collective flow (CU-H06) with sole-admin auto-promote via `leave_collective()` RPC; sole-member rejected with P0001 (migration 017, Fase 10.1).
|
||||||
|
- [new] Dissolve-collective flow (CU-H08) with type-the-name confirmation, via `dissolve_collective()` RPC (migration 018, Fase 10.3).
|
||||||
|
- [new] Section visibility — per-user + per-collective `feature_flags jsonb` + `section_enabled()` STABLE SQL helper using COALESCE precedence (migration 023, Fase 12).
|
||||||
|
- [new] Auto-detect language on first sign-in from `navigator.languages`, gated to fresh accounts (`<60 s old` + `language = 'en'`) (Fase 10.6).
|
||||||
|
- [tweak] Every `created_by`/`updated_by` FK relaxed from `NOT NULL RESTRICT` to `NULL SET NULL` so a user delete cascades cleanly (migration 019, Fase 10.5).
|
||||||
|
|
||||||
|
- Server admin:
|
||||||
|
- [new] Global `server_admin` role (`server_admins` table) orthogonal to `collective_members`; `is_server_admin()` SECURITY DEFINER gate; bootstrap from `SERVER_ADMIN_EMAIL` on first DB init (migration 024, Fase 13).
|
||||||
|
- [new] `/admin` route group with 9 SECURITY DEFINER RPCs (collective lifecycle, member removal, server-level section defaults) + append-only `admin_actions` audit log + last-admin guard (migration 025, Fase 13).
|
||||||
|
- [new] `section_enabled()` extended with a server layer prepended to the user/collective precedence chain (Fase 13.2).
|
||||||
|
|
||||||
|
- i18n:
|
||||||
|
- [new] Paraglide JS messages for `en` + `es`, compiled at build time; `setLanguageTag()` flips the active locale without reload; selector on `/settings` writes to `public.users.language` (Fase 0).
|
||||||
|
- [new] Search + collective-management + PWA copy fully localised in both languages across every fase that added user-facing strings (Fases 4, 10, 14).
|
||||||
|
|
||||||
|
- Infra + deploy:
|
||||||
|
- [new] Local dev stack — Docker compose with Postgres / GoTrue / PostgREST / Realtime / Storage / Kong / Keycloak; `setup.sh` for idempotent first-time bring-up; `Justfile` task runner (Fase 0).
|
||||||
|
- [new] Production stack on ambrosio (OVH VPS) at https://erosi.limonia.net — `infra/scripts/deploy-erosi.sh` (rsync + first-deploy secret generation + every-deploy migration apply + pre-deploy backup + `.deploys.log` append) and `infra/scripts/rollback-erosi.sh` (paired code + DB rollback) (Fase 6 + later iterations).
|
||||||
|
- [new] Kong rate-limit on `/rest/v1/collective_invitations` (20/hour per Authorization header) with `request-transformer` URI rewrite (Fase 6.3).
|
||||||
|
- [new] JWT rotation script (`infra/scripts/rotate-jwt.sh`) + production env template + `rls-audit.sh` curl sweep + `just lighthouse` build-and-score recipe (Fase 6.4 + 6.5).
|
||||||
|
- [new] Internal Caddy on `:3000` fans `/rest|/auth|/realtime|/storage|/graphql|/pg` → Kong, everything else → app; external TLS terminator (off-stack) forwards plain HTTP to ambrosio:3000 (Fase 6 → revision after prod cutover).
|
||||||
|
- [new] Theme tokens stored as RGB triplets so `dark:` variants resolve consistently; three-mode preference (`light` / `dark` / `system`) persisted to `users.theme` + mirrored to `localStorage` with inline anti-FOUC script (migration 015, Fase 9.1).
|
||||||
|
- [new] `sync_conflicts` append-only log + queue-side divergence detection so last-write-wins conflicts are auditable (migration 016, Fase 9.3).
|
||||||
|
- [new] Pre-deploy `pg_dumpall` snapshot kept in `backups/predeploy-*.sql.gz` (newest 10) with paired code/DB rollback via `rollback-erosi.sh --previous` / `--to=<sha>` / `--code-only` (Fase 14 deploy work).
|
||||||
|
- [fix] `docker compose exec -T` calls inside SSH heredoc deploys now redirect stdin per call so the surrounding heredoc isn't drained — silently skipped migrations no longer happen (Fase 8 deploy hardening).
|
||||||
|
- [fix] PostgREST schema cache reload (`NOTIFY pgrst, 'reload schema'`) appended after every deploy and rollback so freshly-added RPCs are visible without a container restart (Fase 15 deploy work).
|
||||||
|
- [fix] WebKit Playwright project gated behind `RUN_WEBKIT=1` so hosts without the GTK libs (libicu74, libwoff1, …) keep `just test-all` green; `*.webkit.test.ts` files excluded from the chromium project (Fase 9.4).
|
||||||
3
Justfile
3
Justfile
@@ -94,8 +94,9 @@ serve: build
|
|||||||
{{dc_dev}} up -d
|
{{dc_dev}} up -d
|
||||||
PORT=3000 node apps/web/build/index.js
|
PORT=3000 node apps/web/build/index.js
|
||||||
|
|
||||||
# Type-check all packages
|
# Type-check all packages (also lints CHANGELOG.md — Fase 17.4.3)
|
||||||
check:
|
check:
|
||||||
|
node scripts/check-changelog.mjs
|
||||||
pnpm turbo run check
|
pnpm turbo run check
|
||||||
|
|
||||||
# Lint all packages
|
# Lint all packages
|
||||||
|
|||||||
109
scripts/check-changelog.mjs
Executable file
109
scripts/check-changelog.mjs
Executable file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Fase 17.4.3 — CHANGELOG.md linter.
|
||||||
|
*
|
||||||
|
* Validates three things and exits non-zero on the first failure:
|
||||||
|
*
|
||||||
|
* 1. Keep-a-Changelog header line is present.
|
||||||
|
* 2. Every release heading matches `## [vX.Y.Z] — YYYY-MM-DD` or the
|
||||||
|
* placeholder `## [Unreleased]`. Pre-release / build-metadata
|
||||||
|
* suffixes are allowed on the version (e.g. `v0.0.0-beta`,
|
||||||
|
* `v1.0.0-rc.0`).
|
||||||
|
* 3. Every bullet inside a release section starts with one of the
|
||||||
|
* three prefixes `[new] / [fix] / [tweak]`. Bullets that introduce
|
||||||
|
* a sub-grouping header (lines ending in `:`) are exempted so the
|
||||||
|
* area-by-area structure of the beta backfill can use plain bullets
|
||||||
|
* as section titles.
|
||||||
|
*
|
||||||
|
* Wired into `just check` and runnable standalone:
|
||||||
|
* node scripts/check-changelog.mjs [path]
|
||||||
|
*
|
||||||
|
* Exit codes:
|
||||||
|
* 0 — file passes all three checks
|
||||||
|
* 1 — usage error (missing file)
|
||||||
|
* 2 — validation failure (one or more issues printed to stderr)
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const repoRoot = resolve(__filename, '..', '..');
|
||||||
|
|
||||||
|
const target = resolve(process.argv[2] ?? resolve(repoRoot, 'CHANGELOG.md'));
|
||||||
|
|
||||||
|
let raw;
|
||||||
|
try {
|
||||||
|
raw = readFileSync(target, 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`check-changelog: cannot read ${target}: ${err.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = raw.split('\n');
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
// Check 1 — Keep-a-Changelog header.
|
||||||
|
if (!raw.includes('Keep a Changelog')) {
|
||||||
|
errors.push('Missing Keep-a-Changelog reference in the header.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk through lines. Track when we are inside a release section.
|
||||||
|
const RELEASE_HEADING = /^## \[(.+?)\](?:\s+—\s+(.+))?$/;
|
||||||
|
const VERSION_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
||||||
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
||||||
|
const BULLET = /^[-*]\s+(.*)$/;
|
||||||
|
const ALLOWED_PREFIXES = ['[new]', '[fix]', '[tweak]'];
|
||||||
|
|
||||||
|
let insideRelease = false;
|
||||||
|
let currentRelease = null;
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const m = line.match(RELEASE_HEADING);
|
||||||
|
if (m) {
|
||||||
|
const [, label, rest] = m;
|
||||||
|
currentRelease = label;
|
||||||
|
insideRelease = true;
|
||||||
|
if (label === 'Unreleased') {
|
||||||
|
// no further checks
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!VERSION_RE.test(label)) {
|
||||||
|
errors.push(
|
||||||
|
`Line ${i + 1}: release label "${label}" does not match vX.Y.Z (semver-with-optional-prerelease).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!rest || !DATE_RE.test(rest)) {
|
||||||
|
errors.push(
|
||||||
|
`Line ${i + 1}: release heading "${label}" is missing the "— YYYY-MM-DD" date segment.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insideRelease) continue;
|
||||||
|
|
||||||
|
const bm = line.match(BULLET);
|
||||||
|
if (!bm) continue;
|
||||||
|
|
||||||
|
const body = bm[1].trim();
|
||||||
|
if (!body) continue;
|
||||||
|
|
||||||
|
// Sub-grouping bullets end with ":" and carry no prefix (e.g. "- Auth + identity:").
|
||||||
|
if (body.endsWith(':')) continue;
|
||||||
|
|
||||||
|
const ok = ALLOWED_PREFIXES.some((p) => body.toLowerCase().startsWith(p));
|
||||||
|
if (!ok) {
|
||||||
|
errors.push(
|
||||||
|
`Line ${i + 1} (in "${currentRelease}"): bullet missing [new]/[fix]/[tweak] prefix — "${body.slice(0, 80)}".`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
for (const e of errors) console.error(`check-changelog: ${e}`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`check-changelog: ${target} OK`);
|
||||||
Reference in New Issue
Block a user