Define a Lexicon for your records
Describe the records your package emits and the extract its binding produces, in one Lexicon document.
Every schema in the system is a Lexicon: the records a package emits, the extract its binding produces, the View its surfaces read, and the manifest itself. A package ships its own Lexicons as Lexicon packages and pins them from the manifest.
One document, two defs
A record Lexicon lives at lexicons/<name>/lexicon.json with a main record and an extract object:
{
"lexicon": 1,
"id": "com.example.blog.post",
"defs": {
"main": {
"type": "record",
"description": "A stable blog post identity and its observed metadata.",
"key": "any",
"record": {
"type": "object",
"required": ["uri", "title"],
"properties": {
"uri": { "type": "string", "format": "uri" },
"title": { "type": "string" },
"publishedAt": { "type": "string", "format": "datetime" },
"author": { "type": "ref", "ref": "com.example.blog.author#recordRef" }
}
}
},
"extract": {
"type": "object",
"description": "Extract produced by the blog binding before transform.",
"required": ["title", "href"],
"properties": {
"title": { "type": "string" },
"href": { "type": "string" },
"date": { "type": "string" },
"author": { "type": "string" }
}
}
}
}- Identity is always
/uri. Links to other records arerefproperties to that record's#recordRef. The manifest never restates either; the runtime reads them from the Lexicon. - The extract is shaped by the page. Field names are what the binding reads, as strings. Required fields are what every entry must have for the extract to count.
- The record is shaped by the product. Types are real (
datetime,uri,integer); the transform does the conversion.
Prefer optional properties for variants you observe over new record types you cannot justify semantically. Keep existing record identities stable across versions unless evidence disproves them.
Wrap it in a Lexicon package
lexicons/<name>/fruitful-package.json:
{ "kind": "lexicon", "name": "com.example.blog.post", "version": "1.0.0", "files": ["lexicon.json"], "document": "lexicon.json" }List it in the definition's packages before the binding and plugin that depend on it.
Lint and generate types
yarn goat lex lint --json lexicons/post/lexicon.json
yarn fruitful lexicon generate feed-packages/<name> --writeGeneration writes typed builders and parsers under authoring/src/generated/. The transform uses them (Post.$build, Post.$parse) so a record that would fail validation fails to compile. The canonical JSON is the runtime authority; generated TypeScript is only the authoring SDK.
Pin it from the manifest
"transform": { "records": ["com.example.blog.post@^1.0.0", "com.example.blog.author@^1.0.0", "app.fruitful.content.webDocument@^0.1.0"] }Shared platform Lexicons (app.fruitful.content.webDocument, app.fruitful.media.*) are already in every registry: pin them, never ship them. Records your package needs beyond those, such as an author or account record, are Lexicons your definition ships, like app.fruitful.identity.account in feed-packages/hacker-news/lexicons/account. See Shared Lexicons.