Skip to content

Constructs

A construct is a named bundle of meshes: a "repo" of feeds grouped into sections. Where a mesh merges sources into one flat feed, a construct keeps the per-mesh grouping, so you can render a multi-section page (a "Daily Brief" with a Models section, a Releases section, and so on) or flatten the whole thing into one feed for the standard serializers.

The Construct type lives in @neurowire/core (packages/core/src/model.ts); fetching lives in fetchConstruct / flattenConstruct (packages/ingest/src/construct.ts).

Shape

A construct is a name plus a list of meshes. Each member is either an inline mesh (self-contained, with its own sources) or a reference to a mesh resolved elsewhere by name.

ts
export const ConstructMemberSchema = z.union([
  z.string().transform((ref) => ({ ref })), // bare string is shorthand for { ref }
  ConstructRefSchema,                        // { ref: "..." }
  MeshSchema,                                // an inline mesh
])
export const ConstructSchema = z.object({
  name: z.string(),
  meshes: z.array(ConstructMemberSchema),
})

A bare string is shorthand for { ref: string }, so a published list of mesh names stays terse:

json
{
  "name": "Daily Brief",
  "meshes": ["ai-news", "security"]
}

Or mix inline meshes with references:

json
{
  "name": "Daily Brief",
  "meshes": [
    {
      "name": "Models",
      "sources": [
        { "name": "Claude Blog", "url": "https://claude.com/blog" },
        { "name": "Simon Willison", "url": "https://simonwillison.net/atom/everything/" }
      ]
    },
    { "ref": "security" }
  ]
}

parseConstruct(data) validates an unknown value into a Construct.

Resolving references: MeshResolver

A construct only carries references; the lookup is supplied by the caller, so core and ingest stay free of any filesystem or registry assumptions.

ts
export type MeshResolver = (ref: string) => Mesh | undefined

fetchConstruct takes an optional resolver. It is required only when the construct actually has { ref } members: an inline mesh passes through unchanged, but a reference with no resolver (or one that resolves to nothing) throws.

createConfigMeshResolver() (in packages/ingest/src/mesh-config.ts) returns a resolver backed by the mesh config directories: explicit dirs, then NEUROWIRE_MESHES, then ~/.config/neurowire/meshes/. Drop a published mesh pack there and a construct can reference it by name.

ts
import { fetchConstruct, createConfigMeshResolver } from '@neurowire/ingest'

const fetched = await fetchConstruct(construct, {
  resolver: createConfigMeshResolver(),
})

fetchConstruct vs flattenConstruct

These are two views of the same data.

fetchConstruct(construct, options) returns a FetchedConstruct: the construct name plus one merged feed per mesh, with grouping preserved.

ts
interface FetchedConstruct {
  name: string
  parts: { mesh: Mesh; feed: NeurowireFeed }[]
}

This is what the grouped HTML rendering uses (a per-mesh section per part).

flattenConstruct(fetched, options) collapses a FetchedConstruct into one NeurowireFeed, tagging every entry with the mesh it came from. The grouping is dropped (NWF, Atom, and JSON Feed cannot express it), so this is the path the feed serializers and the API use.

ts
const fetched = await fetchConstruct(construct, { resolver })
const feed = flattenConstruct(fetched, { limit: 50 })
const atom = serialize(feed, 'atom')

Concurrency control

A construct of many meshes could otherwise open every source of every mesh at once (dozens of connections), and that burst makes slow hosts time out and drop whole meshes. So meshes are fetched with bounded concurrency (concurrency, default 2). Sources within a single mesh are still fetched together in parallel.

Partial-failure semantics

Failures are non-fatal at both levels:

  • A source that fails is logged via onSourceError and skipped (same as a plain mesh).
  • A mesh whose every source fails is logged via onMeshError (default: a one-line stderr warning) and skipped entirely.
  • fetchConstruct throws only when no mesh could be fetched: Construct "<name>": no meshes could be fetched.

FetchConstructOptions also forwards the shared fetch tuning (signal, cache, timeoutMs, retries, backoffMs) and a limit (newest N entries within each mesh). See Fetching.

Named constructs from config

Constructs can be stored as JSON and referenced by name. The directories are searched in order: NEUROWIRE_CONSTRUCTS (: / , separated), then ~/.config/neurowire/constructs/ (or $XDG_CONFIG_HOME/neurowire/constructs). Files are matched as <name>.construct.json then <name>.json. Names must be simple identifiers; path-like names are rejected.

Bundled daily

So that ?src=daily works with no setup, the API ships one built-in construct, daily ("Daily Brief"), with two meshes: a "Models" mesh (Claude Blog, Simon Willison) and a "Releases" mesh (Claude Code Releases). See packages/api/src/constructs.ts. User construct directories take precedence over the bundled defaults.