Skip to content
Open
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
18 changes: 9 additions & 9 deletions docs/atproto-lexicon.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ By defining a lexicon, OXA documents become first-class objects on the AT Protoc

## Lexicon structure

The OXA lexicon is organized into two namespaces:
The OXA lexicon is organized into three namespaces:

| File | NSID | Purpose |
| -------------------------------- | --------------------------- | --------------------------------------------------------------------------------------- |
| `lexicon/document/document.json` | `pub.oxa.document.document` | The `Document` record type — the root object stored in a PDS |
| `lexicon/document/defs.json` | `pub.oxa.document.defs` | Block-level type definitions (`paragraph`, `heading`, `richText`) and the `block` union |
| `lexicon/document/document.json` | `pub.oxa.document` | The `Document` record type — the root object stored in a PDS |
| `lexicon/blocks/defs.json` | `pub.oxa.blocks.defs` | Block-level type definitions (`paragraph`, `heading`, `richText`) and the `block` union |
| `lexicon/richtext/facet.json` | `pub.oxa.richtext.facet` | Facet annotations for inline formatting (`emphasis`, `strong`, `byteSlice`) |

A `Document` record contains an array of `children` (blocks). Each block carries a `text` string and an optional `facets` array that annotates ranges of that text with formatting features.
Expand Down Expand Up @@ -104,7 +104,7 @@ AT Protocol [uses facets instead of a tree](https://www.pfrazee.com/blog/why-fac

```json
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "This is bold and italic text.",
"facets": [
{
Expand Down Expand Up @@ -187,16 +187,16 @@ Produces:

```json
{
"$type": "pub.oxa.document.document",
"$type": "pub.oxa.document",
"children": [
{
"$type": "pub.oxa.document.defs#heading",
"$type": "pub.oxa.blocks.defs#heading",
"level": 1,
"text": "Hello",
"facets": []
},
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "Some emphasized text.",
"facets": [
{
Expand Down Expand Up @@ -248,8 +248,8 @@ The lexicon files are generated from the OXA YAML schema definitions by the code
1. Loads the merged OXA JSON Schema.
2. Classifies each type as inline or block based on the `Inline` and `Block` union definitions.
3. Maps inline types to facet features in `pub.oxa.richtext.facet` (excluding `Text`, which becomes the plain text string).
4. Maps block types to object definitions in `pub.oxa.document.defs`, replacing their inline `children` arrays with `text` + `facets` pairs.
5. Emits the `Document` record type in `pub.oxa.document.document`.
4. Maps block types to object definitions in `pub.oxa.blocks.defs`, replacing their inline `children` arrays with `text` + `facets` pairs.
5. Emits the `Document` record type in `pub.oxa.document`.

To regenerate the lexicon after changing the schema:

Expand Down
22 changes: 11 additions & 11 deletions examples/rfc0003.atproto.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$type": "pub.oxa.document.document",
"$type": "pub.oxa.document",
"title": {
"text": "Water Dissociation: H2O → H+ + OH−",
"facets": [
Expand All @@ -19,13 +19,13 @@
},
"children": [
{
"$type": "pub.oxa.document.defs#heading",
"$type": "pub.oxa.blocks.defs#heading",
"text": "Introduction",
"facets": [],
"level": 1
},
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "Water (H2O) undergoes autoionization, a process in which a water molecule donates a proton to another. The equilibrium constant for this reaction, Kw, is approximately 10−14 at 25 °C.",
"facets": [
{
Expand All @@ -50,15 +50,15 @@
}
]
},
{ "$type": "pub.oxa.document.defs#thematicBreak" },
{ "$type": "pub.oxa.blocks.defs#thematicBreak" },
{
"$type": "pub.oxa.document.defs#heading",
"$type": "pub.oxa.blocks.defs#heading",
"text": "Computing the Equilibrium",
"facets": [],
"level": 2
},
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "The following Python snippet computes Kw from ion concentrations:",
"facets": [
{
Expand All @@ -68,12 +68,12 @@
]
},
{
"$type": "pub.oxa.document.defs#code",
"$type": "pub.oxa.blocks.defs#code",
"value": "H_plus = 1e-7 # mol/L\nOH_minus = 1e-7 # mol/L\nKw = H_plus * OH_minus\nprint(f\"Kw = {Kw:.2e}\") # Kw = 1.00e-14",
"language": "python"
},
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "You can run this with python kw.py. The result confirms the well-known value of Kw.",
"facets": [
{
Expand All @@ -90,15 +90,15 @@
}
]
},
{ "$type": "pub.oxa.document.defs#thematicBreak" },
{ "$type": "pub.oxa.blocks.defs#thematicBreak" },
{
"$type": "pub.oxa.document.defs#heading",
"$type": "pub.oxa.blocks.defs#heading",
"text": "Summary",
"facets": [],
"level": 2
},
{
"$type": "pub.oxa.document.defs#paragraph",
"$type": "pub.oxa.blocks.defs#paragraph",
"text": "This example demonstrates every node type from RFC0003: Heading, Paragraph, Code, ThematicBreak, Text, Emphasis, Strong, Superscript, Subscript, and InlineCode.",
"facets": [
{
Expand Down
2 changes: 1 addition & 1 deletion lexicon/document/defs.json → lexicon/blocks/defs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lexicon": 1,
"id": "pub.oxa.document.defs",
"id": "pub.oxa.blocks.defs",
"defs": {
"richText": {
"type": "object",
Expand Down
6 changes: 3 additions & 3 deletions lexicon/document/document.json → lexicon/document.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"lexicon": 1,
"id": "pub.oxa.document.document",
"id": "pub.oxa.document",
"defs": {
"main": {
"type": "record",
Expand All @@ -26,13 +26,13 @@
},
"title": {
"type": "ref",
"ref": "pub.oxa.document.defs#richText"
"ref": "pub.oxa.blocks.defs#richText"
},
"children": {
"type": "array",
"items": {
"type": "ref",
"ref": "pub.oxa.document.defs#block"
"ref": "pub.oxa.blocks.defs#block"
}
},
"createdAt": {
Expand Down
26 changes: 13 additions & 13 deletions packages/oxa-core/src/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
path: resolve(REPO_ROOT, "lexicon/richtext/facet.json"),
},
defs: {
id: "pub.oxa.document.defs",
path: resolve(REPO_ROOT, "lexicon/document/defs.json"),
id: "pub.oxa.blocks.defs",
path: resolve(REPO_ROOT, "lexicon/blocks/defs.json"),
},
document: {
id: "pub.oxa.document.document",
id: "pub.oxa.document",
path: resolve(REPO_ROOT, "lexicon/document/document.json"),
},
} as const;
Expand Down Expand Up @@ -224,7 +224,7 @@
return JSON.parse(readFileSync(filePath, "utf8")) as LexiconDoc;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > resolves every local and cross-file ref to a known lexicon definition

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ src/convert.test.ts:359:9 ❯ src/convert.test.ts:357:35

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > defines the top-level document record against the shared rich text and block defs

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ getLexiconDefs src/convert.test.ts:236:10 ❯ src/convert.test.ts:339:18

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > parses all Phase 1 lexicon files and gives them the expected lexicon ids

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ src/convert.test.ts:303:23

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > resolves every local and cross-file ref to a known lexicon definition

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ src/convert.test.ts:359:9 ❯ src/convert.test.ts:357:35

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > defines the top-level document record against the shared rich text and block defs

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ getLexiconDefs src/convert.test.ts:236:10 ❯ src/convert.test.ts:339:18

Check failure on line 227 in packages/oxa-core/src/convert.test.ts

View workflow job for this annotation

GitHub Actions / ci

src/convert.test.ts > ATProto lexicon structure > parses all Phase 1 lexicon files and gives them the expected lexicon ids

Error: Failed to read or parse lexicon/document/document.json: ENOENT: no such file or directory, open '/home/runner/work/oxa/oxa/lexicon/document/document.json' ❯ readLexicon src/convert.test.ts:227:11 ❯ src/convert.test.ts:303:23
`Failed to read or parse ${relative(REPO_ROOT, filePath)}: ${message}`,
);
}
Expand Down Expand Up @@ -342,9 +342,9 @@
expect(main.type).toBe("record");
expect(main.key).toBe("tid");
expect(record.required).toEqual(["children", "createdAt"]);
expect(record.properties.title.ref).toBe("pub.oxa.document.defs#richText");
expect(record.properties.title.ref).toBe("pub.oxa.blocks.defs#richText");
expect(record.properties.children.items.ref).toBe(
"pub.oxa.document.defs#block",
"pub.oxa.blocks.defs#block",
);
expect(record.properties.createdAt).toEqual({
type: "string",
Expand Down Expand Up @@ -380,7 +380,7 @@
);

await expect(map(block)).resolves.toEqual({
$type: "pub.oxa.document.defs#paragraph",
$type: "pub.oxa.blocks.defs#paragraph",
id: "para-1",
classes: ["lead"],
data: { align: "left" },
Expand All @@ -402,7 +402,7 @@
});

await expect(map(block)).resolves.toEqual({
$type: "pub.oxa.document.defs#heading",
$type: "pub.oxa.blocks.defs#heading",
id: "intro",
classes: ["hero"],
data: { section: true },
Expand Down Expand Up @@ -464,7 +464,7 @@
);

await expect(convertDocument(document, { createdAt })).resolves.toEqual({
$type: "pub.oxa.document.document",
$type: "pub.oxa.document",
title: {
text: "Hello, World",
facets: [],
Expand All @@ -475,13 +475,13 @@
},
children: [
{
$type: "pub.oxa.document.defs#heading",
$type: "pub.oxa.blocks.defs#heading",
level: 1,
text: "Introduction",
facets: [],
},
{
$type: "pub.oxa.document.defs#paragraph",
$type: "pub.oxa.blocks.defs#paragraph",
text: "This is bold and italic text.",
facets: [
{
Expand All @@ -505,7 +505,7 @@
await expect(
convertDocument(documentNode([]), { createdAt }),
).resolves.toEqual({
$type: "pub.oxa.document.document",
$type: "pub.oxa.document",
children: [],
createdAt,
});
Expand Down Expand Up @@ -539,11 +539,11 @@
);

expect(converted).toEqual({
$type: "pub.oxa.document.document",
$type: "pub.oxa.document",
metadata,
children: [
{
$type: "pub.oxa.document.defs#paragraph",
$type: "pub.oxa.blocks.defs#paragraph",
text: "Keep this paragraph",
facets: [],
},
Expand Down
20 changes: 10 additions & 10 deletions packages/oxa-core/src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,23 +96,23 @@ interface RichText {

type AtprotoParagraph = RichText &
BlockNodeBase & {
$type: "pub.oxa.document.defs#paragraph";
$type: "pub.oxa.blocks.defs#paragraph";
};

type AtprotoHeading = RichText &
BlockNodeBase & {
$type: "pub.oxa.document.defs#heading";
$type: "pub.oxa.blocks.defs#heading";
level: number;
};

type AtprotoCode = BlockNodeBase & {
$type: "pub.oxa.document.defs#code";
$type: "pub.oxa.blocks.defs#code";
value: string;
language?: string;
};

type AtprotoThematicBreak = BlockNodeBase & {
$type: "pub.oxa.document.defs#thematicBreak";
$type: "pub.oxa.blocks.defs#thematicBreak";
};

type AtprotoBlock =
Expand All @@ -122,7 +122,7 @@ type AtprotoBlock =
| AtprotoThematicBreak;

type AtprotoDocument = {
$type: "pub.oxa.document.document";
$type: "pub.oxa.document";
title?: RichText;
metadata?: Record<string, unknown>;
children: AtprotoBlock[];
Expand Down Expand Up @@ -177,10 +177,10 @@ export const compatibleFeatures: Record<

const formattingPropertyNames = ["id", "classes", "data"] as const;
const blockPropertyNames = ["id", "classes", "data"] as const;
const paragraphType = "pub.oxa.document.defs#paragraph" as const;
const headingType = "pub.oxa.document.defs#heading" as const;
const codeType = "pub.oxa.document.defs#code" as const;
const thematicBreakType = "pub.oxa.document.defs#thematicBreak" as const;
const paragraphType = "pub.oxa.blocks.defs#paragraph" as const;
const headingType = "pub.oxa.blocks.defs#heading" as const;
const codeType = "pub.oxa.blocks.defs#code" as const;
const thematicBreakType = "pub.oxa.blocks.defs#thematicBreak" as const;

const encoder = new TextEncoder();

Expand Down Expand Up @@ -414,7 +414,7 @@ export function oxaToAtproto(
options: OxaToAtprotoOptions = {},
): AtprotoDocument {
return {
$type: "pub.oxa.document.document",
$type: "pub.oxa.document",
...getOptionalDocumentFields(session, document),
children: mapKnownBlocks(session, document.children),
createdAt: options.createdAt ?? new Date().toISOString(),
Expand Down
4 changes: 2 additions & 2 deletions packages/oxa/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,15 +77,15 @@ describe("oxa cli", () => {
describe("convert", () => {
const createdAt = "2026-03-22T00:00:00.000Z";
const expectedConverted = {
$type: "pub.oxa.document.document",
$type: "pub.oxa.document",
title: {
text: "CLI Example",
facets: [],
},
metadata: { license: "CC-BY-4.0" },
children: [
{
$type: "pub.oxa.document.defs#paragraph",
$type: "pub.oxa.blocks.defs#paragraph",
text: "Hello from CLI",
facets: [],
},
Expand Down
Loading
Loading