Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>)[key],
(parsed as Record<string, unknown>)[key],
at,
);
if (found) return found;
}
}

/** Reads one kind of record: every `<id>.yaml` file in `dir`. A missing directory is empty. */
function readRecords<T>(
dir: string,
Expand Down Expand Up @@ -47,6 +66,8 @@ function readRecords<T>(
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 };
Expand Down
37 changes: 37 additions & 0 deletions packages/core/test/core.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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([]);
});
Expand Down
Loading