Settle goals, guarantees, HTML scope and release automation in the spec docs
This commit is contained in:
@@ -1,69 +1,118 @@
|
||||
# Working in this repo
|
||||
|
||||
Decisions that a reader would otherwise relitigate. Everything about *using* the library is in
|
||||
`README.md`; what is still to build, and what is still undecided, is in `todo.md`.
|
||||
Decisions a reader would otherwise relitigate. Everything about *using* the library is in
|
||||
`README.md`; what is still to build is in `todo.md`.
|
||||
|
||||
## 1. Two formats, never three
|
||||
## 1. Three formats, ADF is the hub
|
||||
|
||||
ADF and one markdown flavour. **No HTML** — not as an output, not as an intermediate, not as a
|
||||
convenience export. A consumer that wants HTML renders the markdown itself, with its own escaping
|
||||
and its own stylesheet; a consumer that wants neither shows the markdown verbatim, which is what the
|
||||
first one does.
|
||||
|
||||
Three formats would mean six directions to keep lossless instead of two.
|
||||
ADF, one markdown flavour, one HTML dialect. Six directions are exposed, but markdown↔HTML compose
|
||||
through ADF, so four conversions exist to keep correct — never write a fifth. A fourth format
|
||||
(wiki markup, anything else) is refused: each one doubles the directions again.
|
||||
|
||||
## 2. The round-trip is the product
|
||||
|
||||
`markdownToAdf(adfToMarkdown(doc))` must equal `doc`. Anything less and a consumer that lets someone
|
||||
edit a ticket destroys what it could not represent — a panel, a mention, an attachment — in a
|
||||
document it did not author.
|
||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc`. Anything less
|
||||
and a consumer that lets someone edit a document destroys what it could not represent — a panel, a
|
||||
mention, an attachment — in a document it did not author.
|
||||
|
||||
That is why the flavour is *extended*: markdown has no syntax for most of what ADF holds, so the
|
||||
flavour invents it. Designing that syntax is the first real task, and it is open (`todo.md`).
|
||||
When losslessness and readability conflict, losslessness wins. A rare node may look ugly; nothing
|
||||
is ever dropped for looks.
|
||||
|
||||
Two consequences to settle before any node is implemented, not after:
|
||||
|
||||
- **What happens to a node the library does not know.** The documented ADF node set is not the whole
|
||||
schema, and Atlassian adds to it. Whether an unknown node is carried opaquely, refused, or dropped
|
||||
is a correctness decision for the whole library, and it decides the return shape of both functions.
|
||||
- **Whether a lossless document must stay readable to a plain markdown reader.** Anything the flavour
|
||||
invents is noise to a reader that does not know it. How much noise is acceptable bounds the syntax.
|
||||
The other direction is a **canonical fixpoint**, not byte-identity: human markdown normalizes
|
||||
(`*em*` and `_em_` become one ADF doc), converting back yields the library's canonical spelling,
|
||||
and that spelling round-trips byte-identically. Byte-identity both ways would mean storing the
|
||||
author's formatting choices in ADF, which has no place for them.
|
||||
|
||||
Round-trip equality is a property to test over a corpus, not a claim to make in prose.
|
||||
|
||||
## 3. Zero runtime dependencies
|
||||
## 3. Unknown input policy
|
||||
|
||||
- **Unknown ADF node** (Atlassian ships undocumented types and adds more): carried opaquely — the
|
||||
raw JSON rides a dedicated syntax in markdown and HTML, and converts back byte-for-byte. The
|
||||
round-trip holds even for documents newer than the library.
|
||||
- **Unmappable foreign HTML element**: error result naming the element. Arbitrary HTML can express
|
||||
what ADF cannot hold, so import maps a documented element set and refuses the rest — never a
|
||||
silent drop.
|
||||
- **Bare `@name` / `:smile:` typed as plain text**: stays a text node. Only the directive syntax
|
||||
produces mention/emoji/media nodes — resolving names to ids needs I/O, which is the consumer's
|
||||
job (e.g. an autocomplete that inserts the directive).
|
||||
|
||||
## 4. The flavour
|
||||
|
||||
- **Directives**, one grammar for everything markdown lacks: `:::panel info` … `:::` for blocks,
|
||||
`:mention[@Mikael]{id=5b10a2}` style inline. Prior art: CommonMark's generic-directives proposal.
|
||||
- **Plain CommonMark is a subset.** Anything a human types converts per the spec; the flavour only
|
||||
adds syntax, never changes CommonMark meaning.
|
||||
- **Tables**: a simple table (one header row, plain inline cells — no spans, widths, colours, block
|
||||
content) emits a pipe table; anything richer emits the directive form.
|
||||
- **Identity-bearing nodes** (`mention`, `media`, `emoji`) carry their ids in attributes. A
|
||||
converted document is therefore only portable within the site it came from — accepted.
|
||||
|
||||
The HTML dialect mirrors this: semantic elements, stable `adf-*` class names, `data-*` attributes
|
||||
for what HTML cannot express, text content always escaped. No stylesheet ships — styling is the
|
||||
consumer's.
|
||||
|
||||
## 5. Zero runtime dependencies
|
||||
|
||||
Nothing in `dependencies`, ever. TypeScript and whatever the tests need are `devDependencies`, and
|
||||
they never reach a consumer. A markdown parser is exactly the dependency this rule exists to refuse:
|
||||
the flavour is not CommonMark, so a general parser would have to be extended into one anyway.
|
||||
they never reach a consumer. This means the CommonMark parser and the (well-formed) HTML parser are
|
||||
written in this repo — a general parser would have to be extended into the flavour anyway.
|
||||
|
||||
## 4. The package contract
|
||||
## 6. The package contract
|
||||
|
||||
- **ESM only.** No CommonJS build, no dual-package hazard.
|
||||
- **Two entrypoints.** The built JavaScript for ordinary consumers, and the TypeScript source for
|
||||
consumers that run TypeScript directly through Node's type stripping — the first consumer is one,
|
||||
which is why this exists.
|
||||
- **Types for both.** The JavaScript entrypoint ships `.d.ts` beside it; the TypeScript entrypoint is
|
||||
its own types.
|
||||
- **Published to public npmjs as `@larvit/atlassian-adf-converter`**, matching `@larvit/log`. Public
|
||||
means the source is public: the Gitea repo starts private, and going public — with the LICENSE in
|
||||
- **Two entrypoints.** Built JavaScript for ordinary consumers; TypeScript source for consumers
|
||||
running TypeScript directly through Node's type stripping.
|
||||
- **Types for both.** The JavaScript entrypoint ships `.d.ts` beside it; the TypeScript entrypoint
|
||||
is its own types.
|
||||
- **Published to public npmjs as `@larvit/atlassian-adf-converter`**, matching `@larvit/log`.
|
||||
Public means the source is public: the Gitea repo starts private, and going public — LICENSE in
|
||||
place — is a step before the first publish, not after it.
|
||||
- **Exact versions.** `save-exact=true` in `.npmrc`, as in every other repo here.
|
||||
|
||||
## 5. Nothing about any consumer
|
||||
## 7. Nothing about any consumer
|
||||
|
||||
No Jira, no HTTP, no REST response shapes, no plainpages, no issue keys. The library takes a document
|
||||
tree and returns a string, or the reverse. A consumer's concern that leaks in here is a seam nobody
|
||||
declared — and the reason this is a library at all rather than a file in the client that needed it.
|
||||
No Jira client, no HTTP, no REST response shapes, no issue keys — and no actual consumer named
|
||||
anywhere in this repo. Design against the README's personas, not against a product. A consumer's
|
||||
concern that leaks in here is a seam nobody declared.
|
||||
|
||||
## 6. Tests first, in Docker
|
||||
## 8. Semver: the formats are API
|
||||
|
||||
The emitted markdown and HTML are contracts, not just the TypeScript surface. After 1.0: a change
|
||||
that makes previously-emitted output parse differently, or stop parsing, is MAJOR; teaching the
|
||||
parsers new syntax while old output still round-trips is MINOR. Documents in flight — stored,
|
||||
cached, mid-edit — survive upgrades. Pre-1.0, normal 0.x rules apply.
|
||||
|
||||
## 9. Release automation
|
||||
|
||||
- `package.json` version on `main` is the source of truth. CI on every `main` push: tests green
|
||||
and version differs from npm's latest → publish and tag `vX.Y.Z`. A merge without a bump (docs,
|
||||
CI, dep bumps) deploys nothing; the bump in each shipping PR is a deliberate semver judgment.
|
||||
- **Renovate** watches npm devDependencies, Docker image pins and action tags, and automerges
|
||||
every update — majors included — when CI is green.
|
||||
|
||||
## 10. Tests first, in Docker
|
||||
|
||||
Write the test for the behaviour wanted, then implement until it passes. `node --test`, beside the
|
||||
code. Node, tsc and npm never run on the host — a compose service or a `docker run` against a
|
||||
**full patch version** image tag (`node:24.19.0-alpine3.24`, never `node:24`), so the same commit
|
||||
builds the same thing on a different day.
|
||||
code. Node, tsc and npm never run on the host — a compose service or `docker run` against a
|
||||
**full patch version** image tag (`node:24.19.0-alpine3.24`, never `node:24`).
|
||||
|
||||
## 7. Style
|
||||
The corpus, all checked in: hand-built fixtures per node and combination; real (sanitized) ADF
|
||||
captured from live Atlassian APIs, including undocumented nodes; property-based generated ADF trees
|
||||
(generator is a devDependency); the official CommonMark spec suite, run against `markdownToAdf`
|
||||
(mapped) and `markdownToHtml`.
|
||||
|
||||
Two-space indent, alphabetically sorted object keys, strict TypeScript. Failures are values, not
|
||||
exceptions: a function that both returns a result and throws for some inputs has two error channels.
|
||||
## 11. Style
|
||||
|
||||
Two-space indent, alphabetically sorted object keys, strict TypeScript. Failures are values:
|
||||
every conversion returns `Result<T>` — `{ ok: true; value } | { ok: false; error: ConvertError }` —
|
||||
and nothing throws. Input validation is structural only: what the walk needs (a node is an object
|
||||
with a string `type`, known nodes have the attrs read from them), nothing more.
|
||||
|
||||
## 12. Non-goals
|
||||
|
||||
Stated so nobody builds them in: no wiki markup (§1), no network or filesystem I/O, no name→id
|
||||
resolution (§3), no ADF schema validation or exported validator — whether Atlassian accepts a tree
|
||||
is Atlassian's call — no shipped CSS (§4), no streaming APIs and no performance budget: conversions
|
||||
are O(n) with no pathological blowups, and real documents are kilobytes. A CLI is a later goal
|
||||
(`todo.md`), not a non-goal.
|
||||
|
||||
Reference in New Issue
Block a user