The canonical model
Neurowire has exactly one in-memory representation of a feed: the NeurowireFeed. Every parser (RSS, Atom, RDF, JSON Feed, HTML auto-detect, taps) produces this shape, and every serializer (NWF, atom, rss, json, md) consumes it. Nothing else in the system reads the upstream format directly. This single model is the contract that lets any input format turn into any output format.
The model lives in packages/core/src/model.ts and is defined with zod schemas, so the same definition both types the code and validates unknown input at runtime.
Design: list metadata only, no article bodies
Neurowire deliberately models a feed as a list of articles, not the articles themselves. An entry carries title, link, dates, a short summary, authors, and tags. It does not carry the full HTML article body.
This keeps the model small, the outputs compact, and fetching cheap (one listing page or feed, not N article pages). If you want the full text, follow the entry's link.
TIP
This is why the nwf format can be so terse and why a mesh of dozens of sources stays small. The model never holds article content.
NeurowireFeed
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Stable identifier for the feed (usually the source URL). |
title | string | yes | Human-readable feed title. |
home | string | no | The website the feed represents (the "alternate" link). |
self | string | no | The canonical URL of the feed document itself. |
updated | string | yes | Last-updated timestamp (ISO 8601). |
authors | Person[] | no | Feed-level authors. |
generator | { name, version? } | no | What produced the feed. Neurowire stamps its own here. |
entries | NeurowireEntry[] | yes | The articles, newest first by convention. |
NeurowireEntry
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Stable per-entry identifier (see synthetic ids below). |
title | string | yes | Article title. |
link | string | yes | URL of the article. |
published | string | no | First-published timestamp (ISO 8601). |
updated | string | no | Last-updated timestamp (ISO 8601). |
summary | string | no | Short description or excerpt. Not the full body. |
authors | Person[] | no | Per-entry authors. |
tags | string[] | no | Categories or labels. |
source | { name?, url? } | no | Where this entry came from. Used by meshes to tag entries by source. |
Person
| Field | Type | Required |
|---|---|---|
name | string | yes |
url | string | no |
email | string | no |
The zod schemas
The schemas mirror the tables above. EntrySchema and FeedSchema are exported alongside their inferred types, and parseNeurowireFeed(data) validates an unknown value (throwing a ZodError on bad input).
export const EntrySchema = z.object({
id: z.string(),
title: z.string(),
link: z.string(),
published: z.string().optional(),
updated: z.string().optional(),
summary: z.string().optional(),
authors: z.array(PersonSchema).optional(),
tags: z.array(z.string()).optional(),
source: z
.object({ name: z.string().optional(), url: z.string().optional() })
.optional(),
})
export const FeedSchema = z.object({
id: z.string(),
title: z.string(),
home: z.string().optional(),
self: z.string().optional(),
updated: z.string(),
authors: z.array(PersonSchema).optional(),
generator: z.object({ name: z.string(), version: z.string().optional() }).optional(),
entries: z.array(EntrySchema),
})Stable synthetic entry ids (content hashing)
Many sources, especially scraped HTML listings, give an entry no real GUID. To keep deduplication and round-trips stable across every format, Neurowire derives a deterministic id from the entry's content when none is present.
The logic lives in packages/core/src/id.ts:
hashHex(input)is a pure FNV-1a (64-bit) hash returned as a fixed 16-char lowercase hex string. It uses nonode:crypto, so core stays portable.stableId(link, title)hashes\${link}\n${title}`and returnsurn:nwf:<16-char hex>`.
During ingestion, an entry that already has a real id keeps it. An entry with an empty id gets stableId(link, title) stamped in. The same (link, title) always produces the same urn, so the same article keeps the same id across fetches and across output formats.
urn:nwf:9f1c4b2a6d3e0f87TIP
Because the id is derived from the link and title, an article that is re-fetched (or merged into a mesh) is recognized as the same entry and deduplicated, even when the upstream source never gave it an id.