Skip to main content

SPEC_JFM

Constant SPEC_JFM 

Source
pub const SPEC_JFM: &str = "# JFM (JIRA-Flavored Markdown) Specification\n\n## Overview\n\nJFM provides bidirectional conversion between Markdown and Atlassian Document\nFormat (ADF), enabling JIRA Cloud issues and Confluence Cloud pages to be\nread, edited, and updated as local markdown files.\n\n## JFM Document Format\n\nA JFM document consists of YAML frontmatter followed by a markdown body,\nseparated by `---` delimiters. The `type` field in the frontmatter\ndiscriminates between JIRA and Confluence content.\n\n### JIRA Issue\n\n```markdown\n---\ntype: jira\ninstance: https://myorg.atlassian.net\nkey: PROJ-123\nsummary: Issue title here\nstatus: In Progress\nissue_type: Story\nassignee: Alice Smith\npriority: High\nlabels:\n  - backend\n  - auth\n---\n\nMarkdown body content describing the issue.\n```\n\n### Confluence Page\n\n```markdown\n---\ntype: confluence\ninstance: https://myorg.atlassian.net\npage_id: \"12345\"\ntitle: Architecture Overview\nspace_key: ENG\nstatus: current\nversion: 7\n---\n\nPage body content here.\n```\n\n### JIRA Frontmatter Fields\n\n| Field        | Required | Description                                                               |\n|--------------|----------|---------------------------------------------------------------------------|\n| `type`       | Yes      | Always `\"jira\"`                                                           |\n| `instance`   | Yes      | Atlassian Cloud instance URL                                              |\n| `key`        | No       | JIRA issue key (e.g., `PROJ-123`). Absent when creating a new issue.      |\n| `project`    | No       | Project key (e.g., `PROJ`). Used for issue creation when `key` is absent. |\n| `summary`    | Yes      | Issue title/summary                                                       |\n| `status`     | No       | Issue status (read-only from JIRA)                                        |\n| `issue_type` | No       | Issue type (Bug, Story, Task, etc.)                                       |\n| `assignee`   | No       | Assigned user display name                                                |\n| `priority`   | No       | Issue priority level                                                      |\n| `labels`     | No       | List of issue labels                                                      |\n\n### Confluence Frontmatter Fields\n\n| Field        | Required | Description                                          |\n|--------------|----------|------------------------------------------------------|\n| `type`       | Yes      | Always `\"confluence\"`                                |\n| `instance`   | Yes      | Atlassian Cloud instance URL                         |\n| `page_id`    | No       | Confluence page ID. Absent when creating a new page. |\n| `title`      | Yes      | Page title                                           |\n| `space_key`  | Yes      | Space key (e.g., `ENG`)                              |\n| `status`     | No       | Page status (`\"current\"` or `\"draft\"`)               |\n| `version`    | No       | Page version number (for optimistic locking)         |\n| `parent_id`  | No       | Parent page ID                                       |\n\n### Issue Key Validation\n\nIssue keys must match the pattern `^[A-Z][A-Z0-9]+-\\d+$`:\n- Starts with an uppercase letter\n- Followed by uppercase letters or digits\n- A hyphen\n- One or more digits\n\n### Parsing Rules\n\n- Frontmatter must begin at the first line with exactly `---`\n- Frontmatter ends at the next `---` on its own line\n- The body may safely contain `---` (only the first occurrence after the\n  opening delimiter closes the frontmatter)\n- Empty body is valid\n- Trailing newlines are preserved\n- Optional fields omitted from YAML when `None` or empty\n\n## Atlassian Document Format (ADF)\n\nADF is JIRA\'s native rich-text format. JFM converts between markdown and\nADF v1.\n\n### ADF Structure\n\n```json\n{\n  \"version\": 1,\n  \"type\": \"doc\",\n  \"content\": [\n    {\n      \"type\": \"paragraph\",\n      \"content\": [\n        { \"type\": \"text\", \"text\": \"Hello \" },\n        { \"type\": \"text\", \"text\": \"world\", \"marks\": [{ \"type\": \"strong\" }] }\n      ]\n    }\n  ]\n}\n```\n\n### Supported Block Nodes\n\n| ADF Node Type     | Markdown Equivalent                                      |\n|-------------------|----------------------------------------------------------|\n| `heading`         | `# H1` through `###### H6`                               |\n| `paragraph`       | Plain text                                               |\n| `codeBlock`       | Fenced code blocks (`` ``` ``)                           |\n| `bulletList`      | `- item` or `* item`                                     |\n| `orderedList`     | `1. item`                                                |\n| `taskList`        | `- [ ] todo` / `- [x] done`                              |\n| `blockquote`      | `> text`                                                 |\n| `rule`            | `---`, `***`, or `___`                                   |\n| `table`           | Pipe tables or `::::table` directive (see below)         |\n| `mediaSingle`     | `![alt](url){attrs}` with optional `:::caption` block    |\n| `mediaInline`     | `:media-inline[]{attrs}` inline directive                |\n| `blockCard`       | `::card[url]{attrs}` leaf directive                      |\n| `embedCard`       | `::embed[url]{attrs}` leaf directive                     |\n| `panel`           | `:::panel{type=info}` container directive                |\n| `expand`          | `:::expand{title=...}` container directive               |\n| `nestedExpand`    | `:::nested-expand{title=...}` container directive        |\n| `layoutSection`   | `::::layout` with `:::column` children                   |\n| `decisionList`    | `:::decisions` with `- <> item` children                 |\n| `extension`       | `::extension{attrs}` leaf directive                      |\n| `bodiedExtension` | `:::extension{attrs}` container directive                |\n\n### Supported Inline Nodes\n\n| ADF Type          | Markdown Equivalent                                  |\n|-------------------|------------------------------------------------------|\n| `text`            | Plain text (with marks applied)                      |\n| `hardBreak`       | `\\` + newline                                        |\n| `emoji`           | `:name:{shortName=... id=... text=...}`              |\n| `status`          | `:status[text]{color=... style=... localId=...}`     |\n| `date`            | `:date[YYYY-MM-DD]{timestamp=EPOCHMS}`               |\n| `mention`         | `:mention[Name]{id=... userType=... accessLevel=...}`|\n| `inlineCard`      | `:card[url]{localId=...}`                            |\n| `placeholder`     | `:placeholder[text]`                                 |\n| `mediaInline`     | `:media-inline[]{type=... id=... collection=...}`    |\n| `inlineExtension` | `:extension[fallback]{type=... key=...}`             |\n\n### Supported Marks\n\n| Mark Type         | Markdown Equivalent                                     |\n|-------------------|---------------------------------------------------------|\n| `strong`          | `**bold**`                                              |\n| `em`              | `*italic*`                                              |\n| `code`            | `` `code` ``                                            |\n| `strike`          | `~~strikethrough~~`                                     |\n| `link`            | `[text](url)`                                           |\n| `underline`       | `[text]{underline}`                                     |\n| `textColor`       | `:span[text]{color=#rrggbb}`                            |\n| `backgroundColor` | `:span[text]{bg=#rrggbb}`                               |\n| `subsup`          | `:span[text]{sub}` or `:span[text]{sup}`                |\n| `annotation`      | `[text]{annotation-id=... annotation-type=...}`         |\n| `alignment`       | Trailing block attr: `{align=center}`                   |\n| `indentation`     | Trailing block attr: `{indent=N}`                       |\n| `breakout`        | Trailing block attr: `{breakout=wide breakoutWidth=N}`  |\n| `border`          | On media/table cells: `border-color=#hex border-size=N` |\n\n### Unsupported Node Handling\n\nADF nodes that cannot be represented in markdown are serialized as fenced\ncode blocks with language `adf-unsupported`:\n\n````markdown\n```adf-unsupported\n{\"type\":\"unknownNode\",\"attrs\":{\"key\":\"value\"}}\n```\n````\n\nOn conversion back to ADF, these blocks are deserialized and restored to\ntheir original ADF structure, enabling lossless round-trips for unsupported\ncontent.\n\n## Content Model Constraints\n\nADF uses a strict content model: each container node permits only a specific\nset of child node types, and each parent\'s content sequence is constrained by\nquantifiers (`?`, `*`, `+`, `{n}`, `{m,n}`). Atlassian\'s APIs reject\ndocuments that violate the model, often as an opaque HTTP 500 with no\nindication of which nesting was at fault. JFM directives parse permissively \u{2014}\n`:::expand` inside `:::panel` produces well-formed ADF, but the API will\nrefuse it.\n\n### Source of truth\n\nThe full content model for every container node is encoded in\n[`src/atlassian/adf_schema/mod.rs`](../../src/atlassian/adf_schema/mod.rs),\ntranscribed faithfully from the upstream `@atlaskit/adf-schema` npm package\nper [ADR-0023](../adrs/adr-0023.md). The pinned upstream version is recorded\nin the `SCHEMA_VERSION` and `UPSTREAM_TARBALL_SHA256` constants in that\nmodule. Treat the module as authoritative; the prose below is illustrative.\n\nPublic helpers expose the model:\n\n- `adf_schema::allowed_children(parent)` \u{2014} returns the union of allowed\n  direct children for a parent node type, or `None` for leaf / unknown\n  types.\n- `adf_schema::content_model(parent)` \u{2014} returns the full sequence of\n  quantified content terms for a parent (preserves ordering and arity).\n- `adf_schema::permits_child(parent, child)` \u{2014} `true` if `child` is permitted\n  as a direct child of `parent`. Permissive on unknown parents (returns\n  `true`) so that future Atlassian node types do not break round-trips.\n- `adf_schema::validate_document(&doc)` \u{2014} depth-first walker that returns\n  every nesting **and** arity violation in document order, with\n  `parent_type`, `child_type` (or quantifier diagnostic), and an index path\n  from the document root.\n\n### Enforcement on writes\n\nThe validator is wired into every JFM-driven write path so violations abort\nlocally with a clear diagnosis instead of producing an opaque HTTP 500:\n\n- `adf_validated::ValidatedAdfDocument::try_new` is the only constructor for\n  the `ValidatedAdfDocument` newtype that the Confluence and JIRA write APIs\n  accept, making \"I forgot to validate\" a compile error.\n- `omni-dev confluence write` and `omni-dev confluence create` (and their\n  MCP tool equivalents) print every violation via the dry-run helper before\n  any network call.\n- On HTTP 500 from a Confluence write that did pass local validation, the\n  client re-runs `validate_document` against the submitted body and attaches\n  the first violation (with a hint from `adf_hints::hint_for`) to the error\n  via `AtlassianError::ApiRequestFailedWithDiagnosis`.\n\n### Common pitfalls\n\nThese illustrate the kinds of constraint the schema encodes; they are not an\nexhaustive list. Consult the schema module for the full set.\n\n- **`panel`** does not permit `expand`, `nestedExpand`, `panel`,\n  `bodiedExtension`, `blockquote`, `layoutSection`, or `table`. Its content\n  is paragraphs, headings, lists (bullet, ordered, decision, task), code\n  blocks, media, rules, extensions, and block cards.\n- **`expand`** does not permit another `expand`, but **does** permit\n  `nestedExpand` as a child. It also does not permit `bodiedExtension` or\n  `layoutSection`.\n- **`nestedExpand`** has a tighter content model than `expand`: it does not\n  permit `expand`, `nestedExpand`, `table`, `blockCard`, `embedCard`, or\n  `bodiedExtension`. It **does** permit `panel` and `blockquote`.\n- **`tableCell`** and **`tableHeader`** permit `nestedExpand` but **not**\n  `expand`. They also do not permit nested `table` or `layoutSection`. Use\n  `:::nested-expand` instead of `:::expand` inside table cells.\n- **`blockquote`** is restrictive: it permits paragraphs, lists (bullet,\n  ordered), code blocks, media, and extensions only. It does not permit\n  headings, tables, panels, expands, decision lists, task lists, or further\n  blockquotes.\n- **`listItem`** permits paragraphs, code blocks, media, extensions, and\n  nested lists (bullet, ordered, task). It does not permit headings,\n  blockquotes, panels, expands, decision lists, tables, or layout sections.\n- **`layoutSection`** permits only `layoutColumn` children \u{2014} layout sections\n  cannot be nested directly. Use multiple `:::column`s within a single\n  `::::layout` instead.\n- **`decisionItem`** and **`taskItem`** are inline-only \u{2014} they cannot\n  contain block content.\n\n### Workarounds\n\nWhen the desired nesting is rejected, common rewrites are:\n\n- **`expand` inside `panel`**: invert the nesting (place the panel inside\n  the expand), or render the two as siblings.\n- **`expand` inside a table cell**: use `:::nested-expand` instead.\n- **List, decision, or task list inside `> blockquote`**: render the quoted\n  text as a paragraph and place the list as a sibling block.\n- **Nested layout sections**: collapse to a single `::::layout` with\n  multiple `:::column` children.\n- **Rich blocks (expand, panel, layout) inside a table cell**: keep them as\n  siblings of the table rather than embedding them.\n\n### Forward-compatibility notes\n\n- `unsupportedBlock` and `unsupportedInline` (the runtime preservation\n  wrappers behind the `adf-unsupported` fenced block) are accepted under any\n  parent by the validator, regardless of the parent\'s allowed-children set,\n  and count toward the parent\'s arity. This preserves the round-trip\n  guarantee from [ADR-0020](../adrs/adr-0020.md) for nodes the snapshot\n  does not yet model.\n- Unknown parent node types are treated permissively: their subtrees are\n  not walked. A future Atlassian node type therefore does not become a\n  validation failure until its content model is added to the schema.\n\n### Coverage and limits\n\nAs of `SCHEMA_VERSION 52.9.5-2026-05-10`, the validator covers:\n\n- Allowed-children sets for every container node type.\n- Per-term quantifiers and content-term sequences (e.g. empty `bulletList`,\n  two-`media` `mediaSingle`, or a `layoutSection` with one column are all\n  reported as `AdfSchemaViolation::Arity`).\n\nOut of scope and not enforced:\n\n- Mark whitelists (which marks may apply to which nodes).\n- Attribute-value schemas (allowed values for `panel.type`, `status.color`,\n  etc.).\n\n## Generic Directive System\n\nJFM uses the CommonMark Generic Directives proposal to represent ADF-specific\nconstructs that have no native markdown equivalent. Three directive levels\nare supported:\n\n### Inline Directives\n\nSyntax: `:name[content]{attrs}`\n\nUsed for inline semantic elements within text:\n\n```markdown\nThe status is :status[In Progress]{color=blue} and assigned to\n:mention[Alice]{id=abc123}.\n\nThe deadline is :date[2026-04-15].\n\nClick the :placeholder[Type something...] field to begin.\n\nSee :media-inline[]{type=file id=UUID collection=NAME} for details.\n```\n\n- Content in `[...]` is **required**\n- Attributes in `{...}` are optional\n- Name must be alphabetic characters and hyphens\n\n### Leaf Block Directives\n\nSyntax: `::name[content]{attrs}`\n\nUsed for standalone block-level elements:\n\n```markdown\n::card[https://example.com/page]{width=80}\n```\n\n- Exactly two colons (not three)\n- Content in `[...]` is optional\n- Must occupy its own line\n\n### Container Directives\n\nSyntax: `:::name{attrs}` ... `:::`\n\nUsed for block-level containers wrapping other content:\n\n```markdown\n:::panel{type=info}\nThis is an informational panel with **rich** content.\n\n- Item one\n- Item two\n:::\n```\n\n- Three or more colons to open\n- Closed by matching colon count with no name\n- Content between open/close is parsed as markdown\n- Attributes are optional\n\n### Attribute Syntax\n\nAttributes follow Pandoc-style `{key=value flag}` syntax:\n\n```\n{type=info}                          # simple key-value\n{color=\"bright red\"}                 # quoted value with spaces\n{bg=#DEEBFF numbered}               # mixed key-value and flag\n{title=\"Click to expand\"}           # quoted string\n{params=\'{\"jql\":\"project=PROJ\"}\'}   # single-quoted JSON value\n```\n\n- Keys: alphanumeric, hyphens, underscores\n- Values: unquoted (stop at whitespace/`}`) or quoted (single/double)\n- Flags: bare words treated as boolean true\n- Round-trip safe: `parse -> render -> parse` preserves structure\n\n## Markdown to ADF Conversion\n\nThe converter uses a line-oriented parser that processes blocks in order:\n\n1. Headings (`# ` through `###### `)\n2. Horizontal rules (`---`, `***`, `___`)\n3. Container directives (`:::name{attrs}` ... `:::`)\n4. Fenced code blocks (`` ``` ``)\n5. Tables (pipe-delimited with separator row)\n6. Blockquotes (`> `)\n7. Lists (`- `, `* `, `1. `, `- [ ] `, `- [x] `)\n8. Leaf directives (`::name[content]{attrs}`)\n9. Images (`![alt](url)`)\n10. Paragraphs (default fallback)\n\nInline content within paragraphs is parsed for:\n- Bold, italic, code, strikethrough\n- Links and bare URLs\n- Inline directives (status, date, mention, emoji)\n- Bracketed spans with attributes (`[text]{color=red}`, `[text]{annotation-id=...}`)\n\n### ADF to Markdown\n\nBlock nodes are rendered to their markdown equivalents. Inline nodes\nhave marks applied (bold, italic, etc.) and semantic nodes render as\ndirectives.\n\n### Block Attributes\n\nBlock-level attributes can follow a block on a separate line:\n\n```markdown\n# Section Title\n{align=center breakout=wide}\n```\n\nSupported attributes: `align`, `indent`, `breakout`.\n\n### Inline Attribute Marks\n\nBracketed spans `[text]{attrs}` represent inline marks that have no native\nmarkdown syntax. Multiple attributes can be combined in a single span.\n\n#### Underline\n\n```markdown\n[underlined text]{underline}\n```\n\n#### Annotation (Inline Comments)\n\nConfluence inline comments attach an `annotation` mark to highlighted text.\nThe mark links the text span to a comment thread stored in Confluence\'s\ncomment system. JFM preserves these marks for round-trip fidelity:\n\n```markdown\n[highlighted text]{annotation-id=\"abc123\" annotation-type=inlineComment}\n```\n\n- `annotation-id`: the annotation identifier (required)\n- `annotation-type`: the annotation type, typically `inlineComment` (required)\n- Annotations can coexist with other marks (bold, italic, etc.):\n  `[**bold comment**]{annotation-id=\"abc123\" annotation-type=inlineComment}`\n\n## Table Rendering Modes\n\nTables use one of two rendering modes depending on cell complexity:\n\n### Pipe Tables (GFM)\n\nUsed when all cells contain simple inline content (single paragraph, no hard\nbreaks, no cell-level marks, no paragraph localIds) and the first row has at\nleast one `tableHeader`:\n\n```markdown\n| Header 1 | Header 2 |\n| --- | --- |\n| cell | cell |\n```\n\n### Directive Tables\n\nUsed when any cell contains complex content (multiple paragraphs, hard breaks,\ncode blocks, nested lists, border marks, or paragraph-level localIds):\n\n```markdown\n::::table{layout=default}\n:::tr\n:::th{colspan=2}\nHeader spanning two columns\n:::\n:::\n:::tr\n:::td{border-color=#091e42 border-size=2}\nCell with border mark\n:::\n:::td\nSimple cell\n:::\n:::\n::::\n```\n\nTable-level attributes include `layout`, `width`, `numbered`/`numbered=false`,\nand `isNumberColumnEnabled`.\n\n## Media Nodes\n\n### `mediaSingle` with Image\n\nFile-hosted media:\n\n```markdown\n![alt](){type=file id=UUID collection=NAME width=N height=N}\n```\n\nThe `occurrenceKey` attribute is preserved when present on the ADF `media`\nnode:\n\n```markdown\n![alt](){type=file id=UUID collection=NAME occurrenceKey=KEY width=N height=N}\n```\n\nExternal media:\n\n```markdown\n![alt](https://example.com/image.png){layout=center width=600}\n```\n\n### `mediaSingle` with Caption\n\nA `:::caption` block immediately following the image line attaches a caption\nto the `mediaSingle` node:\n\n```markdown\n![alt](){type=file id=UUID collection=NAME}\n:::caption{localId=abc123}\nCaption text with **formatting**\n:::\n```\n\nThe caption\'s `localId` is optional.\n\n### `mediaInline`\n\nInline media uses the `:media-inline` directive:\n\n```markdown\nText with :media-inline[]{type=file id=UUID collection=NAME} embedded.\n```\n\nFor external inline media:\n\n```markdown\nText with :media-inline[]{type=external url=https://example.com/file.pdf alt=document} here.\n```\n\n### Border Mark on Media\n\nThe `border` mark on a media node is expressed as additional attributes on the\nimage:\n\n```markdown\n![alt](){type=file id=UUID collection=NAME border-color=#091e4224 border-size=2}\n```\n\nWhen parsing, `border-color` defaults to `#000000` and `border-size` defaults\nto `1` when only one is present.\n\n## `localId` Preservation\n\nMany ADF nodes carry a `localId` attribute used by JIRA and Confluence for\ntask item state tracking, inline comment anchoring, and other stateful\nfeatures. JFM preserves these for round-trip fidelity.\n\n### Syntax\n\nFor directive-based nodes, `localId` appears as an attribute:\n\n```markdown\n:::expand{title=\"Details\" localId=abc-123}\nContent here\n:::\n```\n\nFor standard markdown nodes (headings, paragraphs), `localId` appears on a\ntrailing block-attributes line:\n\n```markdown\n# Section Title\n{localId=abc-123}\n```\n\nFor list items, `localId` is appended inline to avoid misattribution to the\nparent list node:\n\n```markdown\n- Item text {localId=item-id paraLocalId=para-id}\n```\n\nThe `paraLocalId` attribute preserves the localId of a `paragraph` wrapper\ninside a `taskItem` when the original ADF used paragraph children rather than\ndirect inline content.\n\n### Suppression\n\n- Null UUIDs (`00000000-0000-0000-0000-000000000000`) and empty strings are\n  suppressed during rendering.\n- The `strip_local_ids` render option omits all localIds for clean display\n  output where round-trip fidelity is not needed.\n\n### Special Cases\n\n- `expand` and `nestedExpand` store `localId` as a top-level ADF field\n  (`node.local_id`) rather than inside `attrs`. JFM renders it in the\n  directive attributes alongside `title` and `params`.\n- `listItem` nodes with a `mediaSingle` first child preserve their `localId`\n  in the trailing inline attributes.\n\n## Text Escaping for Round-Trip Safety\n\nPlain text that would be reinterpreted by the markdown parser on the return\ntrip is escaped during ADF-to-markdown rendering. Each escape targets a\nspecific ambiguity:\n\n| Pattern                | Escape                    | Prevents                                  |\n|------------------------|---------------------------|-------------------------------------------|\n| `*` in text            | `\\*`                      | Spurious bold/italic                      |\n| `` ` `` in text        | `` \\` ``                  | Spurious code spans                       |\n| `[` `]` in link text   | `\\[` `\\]`                 | Link syntax ambiguity                     |\n| `http://` `https://`   | `\\http://`                | Auto-link / inlineCard detection          |\n| `:name:` in text       | `\\:name:`                 | Emoji shortcode parsing                   |\n| Trailing double-spaces | `\\ ` (escaped last space) | `hardBreak` misinterpretation             |\n| `\\` in text            | `\\\\`                      | Silent backslash consumption              |\n| Literal newline in text| `\\n` (two characters)     | Paragraph splitting                       |\n| `N. ` at line start    | `N\\. `                    | Ordered list re-parsing (in continuations)|\n| `- ` at line start     | `\\- `                     | Bullet list re-parsing (in continuations) |\n\nEscaping is applied only outside code spans and fenced code blocks, where the\nmarkdown parser would otherwise reinterpret the content.\n\n## Authentication\n\n- **Method**: HTTP Basic Auth (base64-encoded `email:api_token`)\n- **Credential sources** (checked in order):\n  1. Environment variables\n  2. `~/.omni-dev/settings.json` `env` map\n- **Required keys**:\n  - `ATLASSIAN_INSTANCE_URL`\n  - `ATLASSIAN_EMAIL`\n  - `ATLASSIAN_API_TOKEN`\n- Same credentials serve both JIRA and Confluence (same Atlassian instance)\n\n## Error Types\n\n| Error                  | Cause                                          |\n|------------------------|------------------------------------------------|\n| `CredentialsNotFound`  | No credentials configured                      |\n| `ApiRequestFailed`     | HTTP error from API (includes status + body)   |\n| `InvalidDocument`      | JFM parse error (bad YAML, missing delimiters) |\n| `ConversionError`      | ADF conversion failure                         |\n";
Expand description

JFM (JIRA-Flavoured Markdown) specification, embedded from docs/specs/jfm.md.