diff --git a/ROADMAP.md b/ROADMAP.md index 61f209f..93687a1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -112,6 +112,7 @@ Each milestone ends at a gate: tests run, results reported, and the maintainer a - **Written down:** published records are never renamed or deleted; a cancelled edition is marked `cancelled` and a convention that stops is marked `ended`. A second edition in the same city and year adds the organizer's name for it (`example-con-winter-2027`). - **Kept:** feeds are named by region id (`feeds/north.ics`) while region pages use the name (`/regions/north-texas/`); renaming either would break subscribers or links. Month pages (`/calendar/YYYY-MM/`) are a rolling window from the current month and outside the stable contract, so a past month's URL can stop resolving. - **Moving:** the site gets its own domain before 1.0 (#47). Every page and feed URL includes the host, and moving after 1.0 would lean on redirects for every feed subscriber. Calendar UIDs don't include the host, so subscribed events aren't duplicated. +- **2026-10-20** — There is no `pnpm data:format` (#11). Prettier already formats `data/` and CI fails on its layout, and every record listed its fields in schema order, so a formatter would have had almost nothing to rewrite. Instead, loading a record warns when its fields stray from schema order, which catches a draft before it's published. That gives up rewriting records automatically: a warned draft is fixed by hand, and the two editions written as `edition: 2026` rather than `'2026'` stay, since both read the same. - **2026-10-11** — `ROADMAP.md` lives at the repo root (the handoff suggested `docs/ROADMAP.md`) to match the maintainer's repo-first workflow. ## Shipped diff --git a/data/README.md b/data/README.md index accb225..9626cc5 100644 --- a/data/README.md +++ b/data/README.md @@ -64,6 +64,8 @@ last_checked: 2026-10-11 # YYYY-MM-DD, last successful check Fields not marked optional are required. Dates are `YYYY-MM-DD` strings, URLs are `https://`, and text can't be empty. +Write fields in the order the tables list them, so a record's diff shows only what changed: `pnpm data:validate` and `pnpm data:diff` warn when they're out of order. Prettier (`pnpm format`) keeps the rest of the layout consistent. + ### Conventions | Field | What it holds | diff --git a/packages/core/src/load.ts b/packages/core/src/load.ts index 64782cf..9d01f94 100644 --- a/packages/core/src/load.ts +++ b/packages/core/src/load.ts @@ -5,6 +5,25 @@ import type { z } from 'zod'; import { EditionSchema, SeriesSchema, SourceSchema, type Dataset } from './schema.ts'; import type { Issue } from './validate.ts'; +/** Where a record's fields first stray from the schema's order. Prettier formats the rest. */ +function misordered(raw: unknown, parsed: unknown, path = ''): string | undefined { + if (!raw || typeof raw !== 'object' || !parsed || typeof parsed !== 'object') return; + // Parsed records list their fields in schema order, so they show where each belongs. + const expected = Object.keys(parsed); + if (!Array.isArray(raw) && Object.keys(raw).join() !== expected.join()) { + return `${path ? `${path} fields` : 'fields'} are out of order; list them as ${expected.join(', ')}`; + } + for (const key of Object.keys(raw)) { + const at = path ? `${path}.${key}` : key; + const found = misordered( + (raw as Record)[key], + (parsed as Record)[key], + at, + ); + if (found) return found; + } +} + /** Reads one kind of record: every `.yaml` file in `dir`. A missing directory is empty. */ function readRecords( dir: string, @@ -47,6 +66,8 @@ function readRecords( issues.push({ level: 'error', record, message: `id must match the file name "${fileId}"` }); continue; } + const order = misordered(raw, result.data); + if (order) issues.push({ level: 'warning', record, message: order }); records.push(result.data); } return { records, issues }; diff --git a/packages/core/test/core.test.ts b/packages/core/test/core.test.ts index c036218..6a2691a 100644 --- a/packages/core/test/core.test.ts +++ b/packages/core/test/core.test.ts @@ -1,3 +1,6 @@ +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import { @@ -89,6 +92,40 @@ describe('loadDataset', () => { expect(dataset.editions).toEqual([]); }); + it('warns when fields are out of schema order, and still loads the record', () => { + const dir = mkdtempSync(join(tmpdir(), 'conatlas-')); + cpSync(FIXTURES, dir, { recursive: true }); + const rewrite = (file: string, from: string, to: string) => + writeFileSync(join(dir, file), readFileSync(join(dir, file), 'utf8').replace(from, to)); + rewrite( + 'editions/lone-star-anime-fest-2026.yaml', + ' facts: [dates, venue, city, tickets]\n checked_at: 2026-09-30', + ' checked_at: 2026-09-30\n facts: [dates, venue, city, tickets]', + ); + rewrite( + 'sources/lone-star-anime-fest-site.yaml', + 'url: https://example.org/lone-star-anime-fest\ntype: official_site', + 'type: official_site\nurl: https://example.org/lone-star-anime-fest', + ); + const { dataset, issues } = loadDataset(dir); + expect(issues).toEqual([ + { + level: 'warning', + record: 'editions/lone-star-anime-fest-2026', + message: + 'evidence.0 fields are out of order; list them as source, facts, checked_at, excerpt', + }, + { + level: 'warning', + record: 'sources/lone-star-anime-fest-site', + message: + 'fields are out of order; list them as id, series, url, type, supports, last_checked', + }, + ]); + expect(dataset.sources).toHaveLength(2); + rmSync(dir, { recursive: true }); + }); + it('passes cross-record validation', () => { expect(validateDataset(base, TODAY)).toEqual([]); });