Skip to content

Commit 3a9d00a

Browse files
ralyodioclaude
andcommitted
A show's Broadcast section no longer claims a Since the feed cannot know
A feed is a window on a show, the last fifty or hundred episodes, and the oldest item the directory holds says when that window starts, not when the show began. Live, a podcast running for years read "Since: 2026-06", which is exactly the false claim OpenBroadcast forbids a platform to make. The key is now left to the owner to write; the query that fed it goes with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7yeJUHGxA4P5N74xnsRPQ
1 parent a375fa4 commit 3a9d00a

6 files changed

Lines changed: 16 additions & 59 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,8 @@ curl -X POST https://rssamplifier.com/api/discover \
258258
Every author page carries `<link rel="openprofile">` to `/authors/{slug}/openprofile.md`, the
259259
person as one portable file: identity block, Accounts (their `rel="me"` links), Topics (their feeds'
260260
subjects), and a Broadcast section ([OpenBroadcast](https://logicsrc.com/openbroadcast)) for every
261-
podcast or show they publish, with only the facts their own feed states. The file never fills in
261+
podcast or show they publish, with only the facts their own feed states (not `Since`: a feed is a
262+
window on a show, and the oldest episode the directory holds is not when the show began). The file never fills in
262263
what the person did not say: no `Seeking`, `Pays`, `Charges`, no Guest section, and no email even
263264
when the API republishes one.
264265

‎apps/web/src/lib/authorProfile.js‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,16 @@ export async function loadAuthorProfile(slug) {
2525

2626
const feeds = person.feeds ?? [];
2727
const feedIds = feeds.map((f) => String(f.id));
28-
const [profile, topicsByFeed, firstByFeed] = await Promise.all([
28+
const [profile, topicsByFeed] = await Promise.all([
2929
profiles.profileForAuthor(client, String(person.id)),
3030
profiles.keywordsForFeeds(client, feedIds),
31-
profiles.firstPublishedAt(client, feedIds),
3231
]);
3332

3433
const base = siteUrl();
3534
const built = authorProfile({
3635
person,
3736
feeds,
3837
topicsByFeed,
39-
firstByFeed,
4038
base,
4139
overrides: profile?.overrides ?? null,
4240
});

‎apps/web/src/lib/openprofile.js‎

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ import {
2020
* Guest section: those are the person's to write, through the overlay, and
2121
* absent means unstated.
2222
*
23+
* `Since` is deliberately not written either. A feed is a window on a show,
24+
* the last fifty or hundred episodes, and the oldest item the directory holds
25+
* says when that window starts, not when the show did. A podcast running since
26+
* 2019 would read "Since: 2026-06", which is exactly the false claim the spec
27+
* forbids; the owner can write the year themselves.
28+
*
2329
* Email is deliberately not in the generated file even when the table has it.
2430
* The API republishes an address the author published; the profile is a
2531
* document other directories will copy, and the owner adds `Email` to it
@@ -68,18 +74,6 @@ function label(network) {
6874
return names[network] ?? network.charAt(0).toUpperCase() + network.slice(1);
6975
}
7076

71-
/**
72-
* `YYYY-MM` from an ISO stamp, or null when it does not parse.
73-
*
74-
* @param {string|null|undefined} iso
75-
* @returns {string|null}
76-
*/
77-
export function yearMonth(iso) {
78-
if (!iso) return null;
79-
const m = /^(\d{4})-(\d{2})/.exec(String(iso));
80-
return m ? `${m[1]}-${m[2]}` : null;
81-
}
82-
8377
/**
8478
* The generated document, before the owner's corrections.
8579
*
@@ -89,12 +83,11 @@ export function yearMonth(iso) {
8983
* feeds: Array<{ id: string, slug: string, title: string, kind?: string|null, role?: string|null,
9084
* feed_url?: string|null, site_url?: string|null, language?: string|null }>,
9185
* topicsByFeed?: Map<string, string[]>,
92-
* firstByFeed?: Map<string, string>,
9386
* base: string,
9487
* }} input
9588
* @returns {import('@profullstack/openprofile').OpenProfileDoc}
9689
*/
97-
export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed, base }) {
90+
export function generateAuthorProfile({ person, feeds, topicsByFeed, base }) {
9891
const links = (person.links ?? []).filter((l) => l.network !== 'email');
9992
const handleLink = HANDLE_NETWORKS.map((n) => links.find((l) => l.network === n && l.handle)).find(
10093
Boolean,
@@ -136,7 +129,6 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed
136129
Show: f.title,
137130
Kind: BROADCAST_KINDS.get(String(f.kind)) ?? null,
138131
Language: f.language ?? null,
139-
Since: yearMonth(firstByFeed?.get(String(f.id))),
140132
Feed: f.feed_url ?? null,
141133
Listen: f.site_url ?? `${base}/${encodeURIComponent(String(f.slug))}`,
142134
Topics: (topicsByFeed?.get(String(f.id)) ?? []).slice(0, 8).join(', ') || null,
@@ -150,7 +142,6 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, firstByFeed
150142
Show: shows[0].title,
151143
Kind: BROADCAST_KINDS.get(String(shows[0].kind)) ?? null,
152144
Language: shows[0].language ?? null,
153-
Since: yearMonth(firstByFeed?.get(String(shows[0].id))),
154145
Feed: shows[0].feed_url ?? null,
155146
Listen: shows[0].site_url ?? `${base}/${encodeURIComponent(String(shows[0].slug))}`,
156147
Topics: (topicsByFeed?.get(String(shows[0].id)) ?? []).slice(0, 8).join(', ') || null,

‎apps/web/test/openprofile.test.js‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
overridesFromBody,
1010
overridesFromForm,
1111
profileUrl,
12-
yearMonth,
1312
} from '../src/lib/openprofile.js';
1413
import { claimVerdict, isOwner, linksBack } from '../src/lib/profileAuth.js';
1514
import { principalFromToken } from '../src/lib/openaccess.js';
@@ -57,10 +56,9 @@ const TOPICS = new Map([
5756
['f1', ['history', 'mathematics']],
5857
['f2', ['mathematics', 'writing']],
5958
]);
60-
const FIRST = new Map([['f1', '2024-11-15T00:00:00.000Z']]);
6159

6260
test('the generated file says what the author published, and nothing they did not', () => {
63-
const doc = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
61+
const doc = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE });
6462

6563
assert.equal(doc.name, 'Ada Lovelace');
6664
const identity = Object.fromEntries(doc.identity.map((e) => [e.key, e.value]));
@@ -84,12 +82,12 @@ test('the generated file says what the author published, and nothing they did no
8482
Show: 'The Analytical Engine',
8583
Kind: 'podcast',
8684
Language: 'en',
87-
Since: '2024-11',
8885
Feed: 'https://ada.example/podcast/feed.xml',
8986
Listen: 'https://ada.example/podcast',
9087
Topics: 'history, mathematics',
9188
});
9289
assert.equal('Seeking' in shows[0], false);
90+
assert.equal('Since' in shows[0], false, 'the feed window is not when the show started');
9391
assert.equal(doc.sections.some((s) => s.name === 'guest'), false, 'no Guest section unless the person wrote one');
9492

9593
const links = doc.sections.find((s) => s.name === 'links');
@@ -98,7 +96,7 @@ test('the generated file says what the author published, and nothing they did no
9896

9997
test('two owned shows become ### groups in one Broadcast section', () => {
10098
const feeds = [FEEDS[0], { ...FEEDS[2], role: 'owner' }];
101-
const doc = generateAuthorProfile({ person: ADA, feeds, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
99+
const doc = generateAuthorProfile({ person: ADA, feeds, topicsByFeed: TOPICS, base: BASE });
102100
const shows = broadcasts(doc);
103101
assert.deepEqual(shows.map((s) => s.Show), ['The Analytical Engine', 'Somebody Elses Show']);
104102
assert.equal(shows[1].Listen, `${BASE}/other-show`, 'a show with no site listens on its directory page');
@@ -110,7 +108,7 @@ test('the overlay wins per part, the rest stays generated, and the file round-tr
110108
identity: { Email: 'ada@example.com', Location: 'London', Avatar: null },
111109
sections: { guest: '- **Available**: yes\n- **Expertise**: early computing', links: 'none' },
112110
};
113-
const { markdown, doc } = authorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE, overrides });
111+
const { markdown, doc } = authorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE, overrides });
114112
assert.equal(doc.headline, 'Countess, programmer.');
115113
const identity = Object.fromEntries(doc.identity.map((e) => [e.key, e.value]));
116114
assert.equal(identity.Email, 'ada@example.com', 'the owner chose to publish it');
@@ -125,7 +123,7 @@ test('the overlay wins per part, the rest stays generated, and the file round-tr
125123
});
126124

127125
test('a PUT body as Markdown becomes the whole overlay; as JSON it is a patch', () => {
128-
const generated = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, firstByFeed: FIRST, base: BASE });
126+
const generated = generateAuthorProfile({ person: ADA, feeds: FEEDS, topicsByFeed: TOPICS, base: BASE });
129127

130128
const edited = '# Ada Lovelace\n\n- Kind: person\n- Web: https://ada.example\n\nCountess.\n\n## Guest\n\n- **Available**: yes\n';
131129
const fromMd = overridesFromBody({ contentType: 'text/markdown; charset=utf-8', text: edited, existing: null, generated });
@@ -235,7 +233,5 @@ test('an OpenAccess token yields a principal with scopes and an email when the h
235233
});
236234

237235
test('small helpers', () => {
238-
assert.equal(yearMonth('2024-11-15T00:00:00Z'), '2024-11');
239-
assert.equal(yearMonth(null), null);
240236
assert.equal(profileUrl(BASE, 'ada lovelace'), `${BASE}/authors/ada%20lovelace/openprofile.md`);
241237
});

‎packages/db/src/profiles.js‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -143,31 +143,6 @@ export async function profilesForUser(db, userId) {
143143
return /** @type {any} */ (rows);
144144
}
145145

146-
/**
147-
* When each feed first published, as `feed_id -> ISO date`, for the `Since`
148-
* key of a Broadcast section. Feeds with no dated item are absent.
149-
*
150-
* @param {Client} db
151-
* @param {string[]} feedIds
152-
* @returns {Promise<Map<string, string>>}
153-
*/
154-
export async function firstPublishedAt(db, feedIds) {
155-
const out = new Map();
156-
if (feedIds.length === 0) return out;
157-
const { rows } = await db.execute({
158-
sql: `select feed_id, min(published_at) as first_at
159-
from feed_items
160-
where feed_id in (${feedIds.map(() => '?').join(',')})
161-
and published_at is not null and published_at <> ''
162-
group by feed_id`,
163-
args: feedIds,
164-
});
165-
for (const row of rows) {
166-
if (row.first_at) out.set(String(row.feed_id), String(row.first_at));
167-
}
168-
return out;
169-
}
170-
171146
/**
172147
* The topics of several feeds at once, strongest first per feed, as
173148
* `feed_id -> keyword[]`. What the Topics of a profile and the `Topics` key of

‎packages/db/test/profiles.test.js‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,7 @@ test('no row means no profile: the generated file stands', async () => {
6868
assert.equal(await profiles.profileForAuthor(db, ada.id), null);
6969
});
7070

71-
test('the Since and Topics inputs come from the feeds themselves', async () => {
72-
const first = await profiles.firstPublishedAt(db, [String(podcast.id), String(blog.id)]);
73-
assert.equal(first.get(String(podcast.id)), '2024-11-15T00:00:00.000Z');
74-
assert.equal(first.has(String(blog.id)), false, 'a feed with no dated item is absent');
75-
71+
test('the Topics inputs come from the feeds themselves', async () => {
7672
const topics = await profiles.keywordsForFeeds(db, [String(podcast.id), String(blog.id)]);
7773
assert.deepEqual(topics.get(String(podcast.id)), ['history', 'mathematics'], 'category first, then by count');
7874
assert.deepEqual(topics.get(String(blog.id)), ['mathematics']);

0 commit comments

Comments
 (0)