From e844943d46c209c2ed40811ee4a38dcc10fbcbf0 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:24:52 +0200 Subject: [PATCH 1/8] Rank discoverability third among the goals, plan the move to GitHub and the outreach, and drop a text node's content in plain --- AGENTS.md | 2 +- README.md | 23 ++- docs/decisions.md | 68 +++---- .../plain/adf-to-plain-markdown.test.ts | 1 + src/markdown/plain/editor-normal.test.ts | 10 +- src/markdown/plain/editor-normal.ts | 7 +- todo.md | 188 ++++++++++-------- 7 files changed, 159 insertions(+), 140 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b0d3f6f..9e29223 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -169,7 +169,7 @@ reading is the ask. Never ask "A or B?": state the gap, the earlier entries of i nearest text, a candidate entry in that file's voice, and the instance it yields. An entry that keeps collecting instances is wrong: rewrite it. -Which output the audience expects — README goal 5 — is settled by a reader panel rather than +Which output the audience expects — README goal 6 — is settled by a reader panel rather than asked: three fresh-context readers, one per README persona the question serves, each given only `## Audience` and the input, writing what they expect before picking among outputs the goals allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing diff --git a/README.md b/README.md index ba17236..2f27dfa 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ The most useful ADF conversion library available, judged by these goals, in prio 1. **Lossless, and every call returns a result, never a throw.** 2. **ADF is the hub.** -3. **Each format reads and writes as its standard says.** -4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.** -5. **No surprises: output reads and edits the way its audience expects.** -6. **Lossy conversion drops form, never content.** -7. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.** -8. **Fast, and linear in the document's size as JSON.** -9. **Easy to find, and clear at a glance what it does.** +3. **Easy to find, and clear at a glance what it does.** +4. **Each format reads and writes as its standard says.** +5. **Our markdown is CommonMark, extended only where CommonMark has no spelling.** +6. **No surprises: output reads and edits the way its audience expects.** +7. **Lossy conversion drops form, never content.** +8. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.** +9. **Fast, and linear in the document's size as JSON.** ## Audience @@ -74,7 +74,7 @@ if (result.ok) { } ``` -Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole +Serves Goals 1, 2 and 8. Pure functions, each taking a whole document and returning a whole result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF: they keep only what ADF holds, and refuse what `markdownToAdf` or `htmlToAdf` refuses. @@ -96,7 +96,7 @@ htmlToMarkdown(html: string): Result // 0.2.0 ## Plain markdown -Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes +Serves Goal 7. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and dropping the rest: attributes, colours, layout, identity. Content is what a reader of the rendered document sees or follows: its text, images and link targets. It refuses only @@ -136,6 +136,7 @@ read replaces mentions, attachments and macros with text. - A newline in text is a hard break and in an expand's title a space, edge whitespace outside a link or code span is trimmed, carriage returns and null characters are removed, and an empty paragraph drops. +- A text node's `content`, which Atlassian's schema forbids, drops. - An ordered list numbered past `999999999`, or adjacent ordered lists whose numbering does not continue, is one bullet list keeping its numbers as text. - A task list beside a bullet or decision list, or holding a block other than a task, joins one @@ -200,7 +201,7 @@ emit refuses: ## The guarantees -Serves Goals 1, 3 and 4. +Serves Goals 1, 4 and 5. - `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects as `JSON.parse` builds them — every key and value as `doc` holds it, adjacent text nodes, an @@ -244,7 +245,7 @@ Serves Goals 1, 3 and 4. ## The package -Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` +Serves Goal 8. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any diff --git a/docs/decisions.md b/docs/decisions.md index 0429182..4cd196e 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -24,7 +24,7 @@ sanitized and a mention keeps the test user's real account id. ## Markdown in is a canonical fixpoint 2026-08-23, the maintainer; every parsed document has converted back since 2026-10-04. Goals 1 and -4. Valid while markdown input may be written by hand. +5. Valid while markdown input may be written by hand. The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way back yields the library's canonical spelling, and that spelling round-trips byte-identically. @@ -54,7 +54,7 @@ around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text ## An empty key spells `empty` -2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while no attribute value +2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while no attribute value spells an empty object or array. An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the @@ -71,7 +71,7 @@ reads it back as `-0`. The grammar: `spec/flavour.md` §Block nodes and §The Co ## Empty markdown is a document of no blocks -2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires +2026-10-03, a writer panel and the maintainer. Goals 1 and 6. Valid while ADF's schema requires `content` on `doc`. Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document @@ -94,7 +94,7 @@ checking its position, and refusing loses a document ADF itself keeps in an `uns ## The carry fence names the node type -2026-10-03, the maintainer. Goals 1 and 5. Valid while a code fence's info string reads back +2026-10-03, the maintainer. Goals 1 and 6. Valid while a code fence's info string reads back verbatim. The block carry is a code fence whose info string `adf:` names the node's type, its body the @@ -118,7 +118,7 @@ language, and none is empty beside another, since a text node holds text. The gr ## Foreign HTML sorts three ways -2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 4. Valid while ADF holds no node +2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 5. Valid while ADF holds no node for a bare container, a comment or a script. Lands with `todo.md` item 6. Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent @@ -139,14 +139,14 @@ than they buy. ## Names stay text -2026-08-23, the maintainer. Goal 7. Valid while resolving a name to an id needs I/O. +2026-08-23, the maintainer. Goal 8. Valid while resolving a name to an id needs I/O. A bare `@name` or `:smile:` in typed text stays a text node. Only directives produce mention/emoji/media nodes; resolving names to ids is the consumer's job. ## Directives under `!adf:` -2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 4 and 5. Valid while prose does not +2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 5 and 6. Valid while prose does not write `!adf:`. Directives are one grammar for everything markdown lacks, namespaced under `!adf:`: @@ -157,7 +157,7 @@ opener nests by itself and leaf versus container falls out of the node's content ## CommonMark is a subset -2026-08-23, the maintainer. Goal 4. Valid while prose rarely writes the shapes the carve-outs claim. +2026-08-23, the maintainer. Goal 5. Valid while prose rarely writes the shapes the carve-outs claim. Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a directive, a pipe table or a `~~` pair is claimed, and so is a code fence whose info string opens @@ -166,14 +166,14 @@ claims, giving CommonMark its own reader, and `todo.md` item 65 ends the image g ## Tables -2026-08-23, the maintainer. Goal 5. Valid while a pipe table holds only one header row and inline +2026-08-23, the maintainer. Goal 6. Valid while a pipe table holds only one header row and inline cells. One header row plus plain inline cells → pipe table; anything richer → directive form. ## Links -2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 5. Valid while CommonMark's link +2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 6. Valid while CommonMark's link syntax is what readers edit. `[text](url "title")`, or `` for a bare autolink-shaped text, wherever CommonMark spells the @@ -192,7 +192,7 @@ accepted. ## Plain task ids come from position -2026-09-26, spelling 2026-09-29, the maintainer. Goals 6 and 7. Valid while a site rejects a task +2026-09-26, spelling 2026-09-29, the maintainer. Goals 7 and 8. Valid while a site rejects a task node with no `localId`. `plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId` @@ -204,7 +204,7 @@ ids are only skipped. ## A callout title keeps its link targets -2026-09-29, the maintainer. Goals 5 and 6. Valid while an expand's `title` is a string. +2026-09-29, the maintainer. Goals 6 and 7. Valid while an expand's `title` is a string. `plainMarkdownToAdf` writes a link in a folded callout's title as its text and its target in parentheses: `> [!faq]- See [x](http://y)` reads to the title `See x (http://y)`. A link whose text @@ -213,7 +213,7 @@ is its target, with or without `mailto:`, keeps its text alone: `` tit ## The plain flavour's spellings -2026-09-14, panels 2026-09-25 and 2026-09-29, the maintainer. Goals 5 and 6. Valid while GitHub's +2026-09-14, panels 2026-09-25 and 2026-09-29, the maintainer. Goals 6 and 7. Valid while GitHub's renderer is the one the audience's markdown is read in. README §Plain markdown's rows come from a survey of GitHub, GitLab, Gitea, Obsidian, Pandoc, @@ -232,7 +232,7 @@ past `[x]`/`[ ]`, and lifting bare URLs, `@name`, `:shortcode:` or ISO dates int ## The HTML dialect -2026-08-23, the maintainer. Goals 5 and 7. Valid while HTML output is read by consumers styling it +2026-08-23, the maintainer. Goals 6 and 8. Valid while HTML output is read by consumers styling it themselves. The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*` @@ -240,7 +240,7 @@ for what HTML cannot express, text always escaped. No stylesheet ships. ## No runtime dependencies -2026-08-23, the maintainer. Goal 7. Valid while ~20 lines of own code, or a vendored table, do each +2026-08-23, the maintainer. Goal 8. Valid while ~20 lines of own code, or a vendored table, do each job a dependency would. `dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20 @@ -250,7 +250,7 @@ and HTML parsers are written in this repo. ## Standards ship as data 2026-08-30, the CommonMark suite 2026-09-05 and ADF's schemas 2026-09-13, the maintainer. Goals 1, -4 and 7. Valid while each table is fixed data a dependency would only wrap. +5 and 8. Valid while each table is fixed data a dependency would only wrap. A table a standard fixes is data rather than a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own module, so entity decoding is complete without one. @@ -273,7 +273,7 @@ nodes that break it. ## Any ES2022 engine -2026-09-01, the maintainer. Goal 7. Valid while ES2022 is the floor browsers and servers share. +2026-09-01, the maintainer. Goal 8. Valid while ES2022 is the floor browsers and servers share. The library runs on any ES2022 engine, not only Node — a browser as readily as a server. The shipped source is ECMAScript and nothing else: no host import, no host global, no DOM. @@ -287,13 +287,13 @@ never the higher one those repo-only tools want. ## ESM only -2026-08-23, the maintainer. Goal 7. Valid while the audience's toolchains all import ES modules. +2026-08-23, the maintainer. Goal 8. Valid while the audience's toolchains all import ES modules. No CommonJS build, no dual-package hazard. ## One built entrypoint -2026-08-23, the maintainer. Goal 7. Valid while Node refuses to type-strip under `node_modules`. +2026-08-23, the maintainer. Goal 8. Valid while Node refuses to type-strip under `node_modules`. Built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint — Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so it cannot serve @@ -301,7 +301,7 @@ an npm consumer. ## Public on npm -2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 7. Valid while the package's source +2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 8. Valid while the package's source stays public beside it. Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public, @@ -375,7 +375,7 @@ handles one cause alike whichever node, attribute or direction raised it. ## `message` and `path` -2026-09-03, the path 2026-09-23, the maintainer. Goals 1 and 4. Valid while a person fixing the +2026-09-03, the path 2026-09-23, the maintainer. Goals 1 and 5. Valid while a person fixing the input reads `message`. - A message names the violation, not the rule alone — a rule by itself states a truth the reader @@ -389,7 +389,7 @@ input reads `message`. ## Publish on a version bump -2026-08-23, converging 2026-09-03, the maintainer. Goal 7. Valid while CI on `main` holds the npm +2026-08-23, converging 2026-09-03, the maintainer. Goal 8. Valid while CI on `main` holds the npm token. `package.json` version on `main` is the source of truth. CI on `main`: tests green and the version @@ -405,7 +405,7 @@ depend on a store that the gate would then have to keep. ## Docs describe the release being built -2026-09-16, the maintainer. Goal 7. Valid while a bump on `main` publishes. +2026-09-16, the maintainer. Goal 8. Valid while a bump on `main` publishes. Docs on `main` describe the release being built rather than the version npm holds, so they match it the moment the bump publishes; add no interim note marking the gap. @@ -421,7 +421,7 @@ nesting a spelling inside its own kind (`*(*a*)*`) names that mark once. ## The gate runs on Deno and Bun -2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 3 and 7. Valid while the library claims +2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 4 and 8. Valid while the library claims any ES2022 engine. The gate runs the suite under Deno and Bun as well as Node. Bun runs JavaScriptCore, the one engine @@ -433,7 +433,7 @@ over the corpus rather than over a smoke import. ## The gate installs the tarball -2026-09-03, the maintainer. Goal 7. Valid while consumers install the packed package. +2026-09-03, the maintainer. Goal 8. Valid while consumers install the packed package. The gate packs the build and installs the tarball under `package-tests/`, so `files`, `exports` and `types` are proved on the artifact that ships rather than on the source tree a self-reference @@ -445,7 +445,7 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s ## Firefox reads the build -2026-09-04, the maintainer. Goal 7. Valid while the library claims a browser and no other leg runs +2026-09-04, the maintainer. Goal 8. Valid while the library claims a browser and no other leg runs SpiderMonkey. A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and @@ -517,7 +517,7 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr ## The CommonMark suite checks three ways -2026-08-27, the maintainer. Goals 1 and 3. Valid while the suite's answers are HTML ADF cannot be +2026-08-27, the maintainer. Goals 1 and 4. Valid while the suite's answers are HTML ADF cannot be compared against. Each example is a named error or markdown that parses and emits to itself byte for byte; its @@ -564,12 +564,12 @@ once doubled the parser's frames per level. ## No value `JSON.parse` or `structuredClone` builds loops a call -2026-10-04, the maintainer. Goals 1 and 8. Valid while every persona hands in plain data, as +2026-10-04, the maintainer. Goals 1 and 9. Valid while every persona hands in plain data, as `JSON.parse` or `structuredClone` builds it. The guards walk a value by `Object.values` and refuse a cycle in it. An inherited or non-enumerable key, a getter, a Proxy or a custom iterator is the caller's to rule out: no persona builds one. -Goal 8's size counts ADF as JSON, so a shared object costs once per place it is held. +Goal 9's size counts ADF as JSON, so a shared object costs once per place it is held. ## Nothing spreads an unbounded array @@ -589,7 +589,7 @@ termination is the loop's own check. ## Readers scan by index -2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 8. Valid while the pipeline persona +2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 9. Valid while the pipeline persona feeds documents nobody typed. A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line @@ -601,7 +601,7 @@ two cannot disagree — which is what makes the kept value a memo rather than a ## The spelling memo -2026-09-19, the maintainer. Goal 8. Valid while the `commonMarkSpelling` ask spells a node once +2026-09-19, the maintainer. Goal 9. Valid while the `commonMarkSpelling` ask spells a node once per level above it otherwise. The parse and the plain reduction keep each node's readable spelling in a memo, so the @@ -613,7 +613,7 @@ one. Only what succeeded is kept, so no path minted at another position is ever ## Cost fixes are measured, never timed -2026-09-18, the maintainer and the stability-reviewer. Goal 8. Valid while Goal 8 promises growth +2026-09-18, the maintainer and the stability-reviewer. Goal 9. Valid while Goal 9 promises growth rather than a figure. A cost fix that changes no behaviour lands on the suite staying green with no fixture output @@ -632,7 +632,7 @@ refuses it — which is how the whitespace carry finds a line edge. ## Emphasis follows CommonMark's matching -2026-08-27, the maintainer. Goals 1 and 3. Valid while CommonMark's emphasis rules are the +2026-08-27, the maintainer. Goals 1 and 4. Valid while CommonMark's emphasis rules are the reader's. Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text @@ -641,7 +641,7 @@ only ones in play, and a pair that matching hands to another delimiter rides the ## Readable spellings take the `try` prefix -2026-09-21, the maintainer. Goals 1 and 5. Valid while a readable spelling's refusal would cost a +2026-09-21, the maintainer. Goals 1 and 6. Valid while a readable spelling's refusal would cost a document the general form spells. A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the diff --git a/src/markdown/plain/adf-to-plain-markdown.test.ts b/src/markdown/plain/adf-to-plain-markdown.test.ts index 2076b8e..2a37a48 100644 --- a/src/markdown/plain/adf-to-plain-markdown.test.ts +++ b/src/markdown/plain/adf-to-plain-markdown.test.ts @@ -314,6 +314,7 @@ test('breaks a line at a newline and trims whitespace at every edge CommonMark s assert.equal(plain(node('heading', { level: 1 }, text(' h\ni '))), '# h i\n') assert.equal(plain(paragraph(text('a\r\u0000b'))), 'ab\n') assert.equal(plain(paragraph(text('x', link('/u', 't\r\u0000')))), '[x](/u "t")\n') + assert.equal(plain(paragraph({ content: [text('dropped')], text: 'a', type: 'text' }, text('b'))), 'ab\n') }) test('keeps a code span opening a line, whose backticks open no fence', () => { diff --git a/src/markdown/plain/editor-normal.test.ts b/src/markdown/plain/editor-normal.test.ts index 851c660..37dec43 100644 --- a/src/markdown/plain/editor-normal.test.ts +++ b/src/markdown/plain/editor-normal.test.ts @@ -77,9 +77,9 @@ test('normalizes blocks and mark attributes nesting far past the levels a recurs assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab']) }) -test('joins a text node holding content to no neighbour, so its content is kept', () => { - const holding: AdfNode = { content: [{ text: 'kept', type: 'text' }], text: 'a', type: 'text' } - for (const content of [[holding, { text: 'b', type: 'text' }], [{ text: 'b', type: 'text' }, holding]]) { - assert.deepEqual(toEditorNormal({ content: [{ content, type: 'paragraph' }], type: 'doc', version: 1 }).content, [{ content, type: 'paragraph' }]) - } +test('drops the content a text node holds, and joins the node as any other', () => { + const holding: AdfNode = { content: [{ text: 'dropped', type: 'text' }], text: 'a', type: 'text' } + const normal = (content: AdfNode[]): AdfNode[] | undefined => toEditorNormal({ content: [{ content, type: 'paragraph' }], type: 'doc', version: 1 }).content + assert.deepEqual(normal([holding, { text: 'b', type: 'text' }]), [{ content: [{ text: 'ab', type: 'text' }], type: 'paragraph' }]) + assert.deepEqual(normal([{ text: 'b', type: 'text' }, holding]), [{ content: [{ text: 'ba', type: 'text' }], type: 'paragraph' }]) }) diff --git a/src/markdown/plain/editor-normal.ts b/src/markdown/plain/editor-normal.ts index 3877967..a5fc8ba 100644 --- a/src/markdown/plain/editor-normal.ts +++ b/src/markdown/plain/editor-normal.ts @@ -7,9 +7,9 @@ type JsonContainer = JsonValue[] | { [key: string]: JsonValue } type NodeHolder = { content?: AdfNode[] } -// The editor's rule is the reader's over the pair's editor-normal forms; a text node holding content never joins, so its content is kept. +// The editor's rule is the reader's over the pair's editor-normal forms. export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean { - if (previous.type !== 'text' || node.type !== 'text' || nodeContent(previous).length > 0 || nodeContent(node).length > 0) return false + if (previous.type !== 'text' || node.type !== 'text') return false return joinsWhenRead(normalNode(previous), normalNode(node)) } @@ -25,7 +25,8 @@ export function toEditorNormal(document: AdfDocument): AdfDocument { if (content.length === 0) continue entry.holder.content = content.map((source) => { const holder = normalNode(source) - pending.push({ holder, source }) + // Atlassian's schema allows a text node no content, so the editor keeps none. + if (source.type !== 'text') pending.push({ holder, source }) return holder }) } diff --git a/todo.md b/todo.md index d462557..168113a 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 70` +`Next ID = 74` | Goal | W | |---|---| @@ -24,38 +24,40 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| -| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 | +| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | | 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | | 67 | 0.2.0 | defect | **Push an expand title's leaves one at a time, so a title carrying many text nodes returns a result.** | 1 | 1 | 4 | 9 | 1 | 24.8 | -| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 | -| 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 7 | 23.0 | +| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | +| 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 8 | 23.0 | | 66 | 0.2.0 | defect | **Refuse an attribute value that is not a plain object, array or JSON primitive.** | 3 | 2 | 5 | 8 | 1 | 22.5 | -| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 | -| 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 3, 4, 6 | 18.7 | +| 71 | 0.2.0 | | **Compare the package with the other ADF converters in the README, each claim checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 | +| 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 4, 5 | 20.3 | | 68 | 0.2.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 | -| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 3, 4 | 17.2 | +| 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 4, 5, 7 | 17.1 | +| 70 | 0.2.0 | | **Give `package.json` the `keywords` someone searching npm for an ADF converter types.** | 1 | 1 | 4 | 6 | 3 | 16.1 | +| 72 | 0.2.0 | | **Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it.** | 5 | 6 | 5 | 8 | 3 | 16.0 | +| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 | | 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | -| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 | -| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 | -| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 | +| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 | +| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 | | 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | -| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 | +| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 6 | 10.0 | | 62 | 0.2.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | +| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 4, 5, 7 | 9.0 | +| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 3 | 26.0 | +| 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 6 | 8 | 3 | 23.0 | +| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 3 | 21.0 | | 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | -| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 | -| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 | -| 42 | 0.3.0 | | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 8 | 12.5 | -| 48 | 0.3.0 | | **Keep the release path publishing past npm's bypass-2FA token retirement.** | 4 | 4 | 8 | 3 | 9 | 11.7 | +| 48 | 0.3.0 | | **Keep the release path publishing past npm's bypass-2FA token retirement.** | 4 | 4 | 8 | 3 | 3 | 15.7 | +| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | +| 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 | | 31 | 0.3.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 | -| 33 | 0.3.0 | | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 8 | 10.7 | -| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 7, 9 | 10.3 | -| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 9 | 10.3 | -| 69 | 0.3.0 | question | **Decide whether `adfToPlainMarkdown` keeps a text node's `content`, and keep it or name the drop in the README.** | 2 | 2 | 3 | 5 | 6 | 7.9 | +| 42 | 0.3.0 | | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | | 56 | 0.3.0 | principle | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 4 | 5 | 2 | 4 | 1 | 6.8 | -| 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 7 | 1.2 | -| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 7 | -2.2 | -| 8 | 0.4.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 9 | 10.6 | +| 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 8 | 0.6 | +| 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 8 | -2.6 | +| 8 | 0.4.0 | | **Ship a CLI.** | 3 | 7 | 7 | 6 | 3 | 18.6 | ## Details @@ -99,6 +101,14 @@ typed array in `attrs`, as `structuredClone` keeps it, passes the guard and is w key per element: `ok: true` with content lost. Accept an object whose prototype is `Object.prototype` or `null`, and an array. Found by the stability review of item 63, 2026-10-04. +### 71. Compare the package with the other ADF converters in the README, each claim checked against that package's own docs or behaviour. + +Lands after item 7, whose HTML the comparison names. A table: which directions each converts, +whether the round-trip holds, what an unknown node does, the formats, runtime dependencies and +size. Candidates are the packages an npm search for ADF and markdown returns, marklassian among +them. Every cell cites that package's README or a run against its published version, since a +reader relies on it. Found while planning discoverability, 2026-10-04. + ### 43. Give each markdown input its own reader, strict to its own standard. Today `markdownToAdf` reads CommonMark and the lossless flavour as one input: text shaped like a @@ -107,19 +117,6 @@ a code fence whose info string opens `adf:` becomes the block carry where Common caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. -### 65. Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title. - -Both are refused today with `unmappable-image`: 14 examples in -`corpus/commonmark-spec/refusals.json`, a bot's `See ![diagram](url) here` among them. `mediaInline` -takes only Atlassian media ids, never a URL. An image not alone in its paragraph reads as its alt -text, linked to its URL. Its title becomes the link's `title`. An empty alt text reads as the URL, -as item 50 reads an empty link. A titled image alone in its paragraph reads as the image, with its -title as a `caption` (the maintainer, 2026-10-04). By Goal 4, the lossless flavour reads both as -CommonMark does. An image inside a link: `[![moon](moon.jpg)](/uri)` (spec example 517). Alone in -its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's `media` takes. Not -alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A -writer panel chose this reading 3 of 3 (2026-10-04), by Goal 5, which outranks Goal 6. - ### 68. Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`. `refuseContentAndText` (`emit/inline-line.ts`) and `emitDirectiveBlock` (`emit/adf-to-markdown.ts`) @@ -128,10 +125,41 @@ round-trips them; `docs/decisions.md` §The code list gives a cause the carry an them from the emitting half of the README's `unsupported-node-shape` row. Found by the README-goals audit, 2026-10-04. +### 65. Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title. + +Both are refused today with `unmappable-image`: 14 examples in +`corpus/commonmark-spec/refusals.json`, a bot's `See ![diagram](url) here` among them. `mediaInline` +takes only Atlassian media ids, never a URL. An image not alone in its paragraph reads as its alt +text, linked to its URL. Its title becomes the link's `title`. An empty alt text reads as the URL, +as item 50 reads an empty link. A titled image alone in its paragraph reads as the image, with its +title as a `caption` (the maintainer, 2026-10-04). By Goal 5, the lossless flavour reads both as +CommonMark does. An image inside a link: `[![moon](moon.jpg)](/uri)` (spec example 517). Alone in +its paragraph, it reads as the image with a `link` mark to `/uri`, which ADF's `media` takes. Not +alone in its paragraph, the image reads as its alt text linked to `/uri`, and its URL drops. A +writer panel chose this reading 3 of 3 (2026-10-04), by Goal 6, which outranks Goal 7. + +### 70. Give `package.json` the `keywords` someone searching npm for an ADF converter types. + +`package.json` has none, and npm's search ranks on them. Take the terms the other ADF converters on +npm list and the names the README uses — ADF, Atlassian Document Format, markdown, CommonMark, HTML +— so a search for any of them can reach the package. Found while planning discoverability, +2026-10-04. + +### 72. Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it. + +The maintainer's call, 2026-10-04: GitHub is where an ADF converter is searched for, and the Gitea +copy keeps the move back open. The workflows move to `.github/workflows/` with `ci.sh` and +`publish.sh` unchanged; `package.json`'s `repository`, `bugs` and `homepage`, the README's, +CHANGELOG's and MIGRATION's links, and AGENTS.md's forge commands and merge style follow. +`docs/decisions.md` gains the entry "GitHub is canonical, and Gitea a pull mirror of it" (Goal 3), +and every entry naming Gitea Actions is revised. Creating the GitHub repository, its `NPM_TOKEN` +secret, and re-creating the Gitea copy as a mirror — Gitea converts a repository to a mirror only +that way — are the maintainer's. + ### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`. Lands after item 43. Today `- a` then `+ b`, or `1.` then `1)`, reads as one list; CommonMark reads -two (spec examples 301 and 302). Goal 4 settles it for the lossless flavour too: CommonMark spells +two (spec examples 301 and 302). Goal 5 settles it for the lossless flavour too: CommonMark spells adjacent lists by changing the marker, so `adfToMarkdown` and `adfToPlainMarkdown` alternate `-` and `*` between adjacent bullet lists and `.` and `)` between adjacent ordered lists, and `!adf:listBreak` retires. `*` is the maintainer's pick (2026-10-04). Breaking, so it ships beside @@ -159,12 +187,6 @@ example 540); lowercasing and then uppercasing folds it. Breaking, so it ships b `MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, which counts one fewer. -### 53. Put a block's `marks` spelling to the writer panel and adopt its pick. - -Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as -escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table -gains its row. - ### 60. Collect the questions `parse/` asks `emit/` into one named module. `parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported @@ -177,14 +199,11 @@ Eight directive files sit across three directories, and the `markdown/` root hol needs `Read` and `Flavour` out of the plain flavour and `markdown/`; it needs the carry and the mark spellings too, which item 7 moves where it learns what HTML shares. -### 50. Read `[](/url)` and `[]()` as CommonMark's empty link. +### 53. Put a block's `marks` spelling to the writer panel and adopt its pick. -Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a -link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04): -`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals -3 and 6. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its -`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, -which counts one fewer. +Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as +escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table +gains its row. ### 62. Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`. @@ -194,10 +213,14 @@ the `inlineLeaves` use. Writing: `spellPlainBlock`, `quotedUnder`, `tryTaskList` `plain/conventions.ts`, which `emit/` imports them from; item 59 moves `Flavour`. Every comprehension reader on 2026-10-03 named the plain flavour's spread across three directories. -### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. +### 50. Read `[](/url)` and `[]()` as CommonMark's empty link. -`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values -already escape it. +Both stay literal text today (spec examples 484 and 487). ADF holds no empty text node to carry a +link mark, so a link whose text is empty takes its URL as its text (the maintainer, 2026-10-04): +`[](/url)` reads as `/url` linked to `/url`. `[]()` has no URL to show; the chunk settles it by Goals +4 and 7. Breaking, so it ships beside item 43: `MIGRATION.md`'s Readings table gains its row. Its +`pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, +which counts one fewer. ### 47. Open the README with what the package is, what it does and for whom. @@ -208,17 +231,16 @@ pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for any of these names finds the package. -### 34. Read emphasis flanking by the whole character beside an astral symbol. +### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions. -Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an -astral symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as -punctuation — and, where they do, read the code point, with a fixture per direction. +Lands after `0.2.0` is published and items 71 and 72 land, so each post links the GitHub +repository and the comparison. We draft each post for the place it goes; the maintainer posts +them. Found while planning discoverability, 2026-10-04. -### 42. Trim a text leaf's trailing blanks in linear time. +### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. -`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic -in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in -`adfToPlainMarkdown`. Scan backward, as the expand title's trim does. +`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values +already escape it. ### 48. Keep the release path publishing past npm's bypass-2FA token retirement. @@ -234,6 +256,22 @@ added Gitea or self-hosted OIDC, and otherwise whether the release moves to the which fits badly with publish-on-merge, and is the maintainer's trade to weigh. The Goals and G cells are provisional: no README goal covers the release path. +### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. + +Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the +tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the +figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since +ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build +or report the unminified gzip). The figure lands in README §The package beside the "no runtime +dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 +kB. + +### 34. Read emphasis flanking by the whole character beside an astral symbol. + +Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an +astral symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as +punctuation — and, where they do, read the code point, with a fixture per direction. + ### 31. Make the branch-coverage figure repeat across runs of an unchanged tree. Three Node test legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and @@ -245,33 +283,11 @@ moves upward, so the first raise to the measured figure reddens a run that chang measurement repeatable, or state the number the floor may be raised to and why it is not the measured one. -### 33. Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`. +### 42. Trim a text leaf's trailing blanks in linear time. -`adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking -cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and the plain -reduction's `spellableLine` drops one mark per re-emit the same way. An inline node whose -attributes no spelling writes re-emits the line the same way before riding the carry. Make all -three linear. - -### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. - -Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the -tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the -figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since -ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build -or report the unminified gzip). The figure lands in README §The package beside the "no runtime -dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 -kB. - -### 69. Decide whether `adfToPlainMarkdown` keeps a text node's `content`, and keep it or name the drop in the README. - -`nodeLeaves` (`plain/inline-reduction.ts`) keeps only a text node's `text`, so -`{ text: 'a', content: [{ text: 'kept', type: 'text' }] }` writes `a`, though `toEditorNormal` -keeps that content (`plain/editor-normal.ts`) and an unknown node's children keep their text. -Atlassian's schema allows no `content` on a text node, so no Atlassian reader saw it; an -export/indexing tool fed a looser producer loses searchable text. Goal 6 reads two ways here: keep -the children's text as an unknown node's, or name the drop in the README's plain section. Found by -the product-owner review, 2026-10-04. +`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic +in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in +`adfToPlainMarkdown`. Scan backward, as the expand title's trim does. ### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes. -- 2.52.0 From be54d24dc0f5b1b8aa60430415d52a54932f9577 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:28:14 +0200 Subject: [PATCH 2/8] Answer stability round 1: item 33 returns as a defect, and item 42 takes the same mark --- todo.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/todo.md b/todo.md index 168113a..6f8ee87 100644 --- a/todo.md +++ b/todo.md @@ -53,7 +53,8 @@ | 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | | 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 | | 31 | 0.3.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 | -| 42 | 0.3.0 | | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | +| 42 | 0.3.0 | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | +| 33 | 0.3.0 | defect | **Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 | | 56 | 0.3.0 | principle | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 4 | 5 | 2 | 4 | 1 | 6.8 | | 57 | 0.3.0 | principle | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 3 | 1 | 3 | 8 | 0.6 | | 58 | 0.3.0 | principle | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 4 | 6 | 1 | 2 | 8 | -2.6 | @@ -289,6 +290,14 @@ measured one. in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in `adfToPlainMarkdown`. Scan backward, as the expand title's trim does. +### 33. Emit a line in time linear in its mark runs, in `adfToMarkdown` and `adfToPlainMarkdown`. + +`adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking +cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and the plain +reduction's `spellableLine` drops one mark per re-emit the same way. An inline node whose +attributes no spelling writes re-emits the line the same way before riding the carry. Make all +three linear. + ### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes. Technical principle "One owner per value": the `Walk` record passes through twelve functions that -- 2.52.0 From b36f6fbeef5c1c4e39174d4380c4e603a4fc7be2 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:28:14 +0200 Subject: [PATCH 3/8] Answer product-owner round 1: the README opener and bundle size move to 0.2.0, the comparison and announcement wait for them, and the GitHub move carries issues, releases and trusted publishing --- todo.md | 85 +++++++++++++++++++++++++-------------------------------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/todo.md b/todo.md index 6f8ee87..c4567c7 100644 --- a/todo.md +++ b/todo.md @@ -24,19 +24,21 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| +| 47 | 0.2.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 3 | 26.0 | | 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | | 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | | 67 | 0.2.0 | defect | **Push an expand title's leaves one at a time, so a title carrying many text nodes returns a result.** | 1 | 1 | 4 | 9 | 1 | 24.8 | | 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | | 64 | 0.2.0 | defect | **Read back unchanged on V8 every JSON key the emitter writes, with a fixture whose key holds `\`, `"` or a control character.** | 4 | 4 | 6 | 8 | 1, 8 | 23.0 | | 66 | 0.2.0 | defect | **Refuse an attribute value that is not a plain object, array or JSON primitive.** | 3 | 2 | 5 | 8 | 1 | 22.5 | -| 71 | 0.2.0 | | **Compare the package with the other ADF converters in the README, each claim checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 | +| 71 | 0.2.0 | | **Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour.** | 1 | 3 | 6 | 7 | 3 | 21.2 | | 43 | 0.2.0 | decision | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 4, 5 | 20.3 | | 68 | 0.2.0 | | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 3 | 3 | 4 | 7 | 1 | 18.2 | | 65 | 0.2.0 | defect | **Read an image not alone in its paragraph as its alt text linked to its URL, and a titled image alone in its paragraph as the image captioned with its title.** | 5 | 5 | 7 | 7 | 4, 5, 7 | 17.1 | | 70 | 0.2.0 | | **Give `package.json` the `keywords` someone searching npm for an ADF converter types.** | 1 | 1 | 4 | 6 | 3 | 16.1 | | 72 | 0.2.0 | | **Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it.** | 5 | 6 | 5 | 8 | 3 | 16.0 | | 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 | +| 46 | 0.2.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | | 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 | | 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 | | 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 4, 5 | 10.9 | @@ -45,12 +47,9 @@ | 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 6 | 10.0 | | 62 | 0.2.0 | decision | **Move the plain flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/plain/`, so `emit/` no longer imports `plain/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | | 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 4, 5, 7 | 9.0 | -| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 3 | 26.0 | | 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 6 | 8 | 3 | 23.0 | | 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 3 | 21.0 | | 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | -| 48 | 0.3.0 | | **Keep the release path publishing past npm's bypass-2FA token retirement.** | 4 | 4 | 8 | 3 | 3 | 15.7 | -| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 | | 34 | 0.3.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 4, 5 | 11.3 | | 31 | 0.3.0 | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 3 | 3 | 4 | 1 | 11.2 | | 42 | 0.3.0 | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 9 | 10.5 | @@ -62,6 +61,15 @@ ## Details +### 47. Open the README with what the package is, what it does and for whom. + +It opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being +decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why" +note left. The badges are npm's version and the GitHub Actions status, so it lands after item 72. The README names the lossy +pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes and reads — GitHub +Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for +any of these names finds the package. + ### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF. Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against @@ -102,13 +110,13 @@ typed array in `attrs`, as `structuredClone` keeps it, passes the guard and is w key per element: `ok: true` with content lost. Accept an object whose prototype is `Object.prototype` or `null`, and an array. Found by the stability review of item 63, 2026-10-04. -### 71. Compare the package with the other ADF converters in the README, each claim checked against that package's own docs or behaviour. +### 71. Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour. -Lands after item 7, whose HTML the comparison names. A table: which directions each converts, +Lands after items 7, 46 and 47: the table names HTML, compares item 46's size figure, and sits in the README item 47 reshapes. A table: which directions each converts, whether the round-trip holds, what an unknown node does, the formats, runtime dependencies and size. Candidates are the packages an npm search for ADF and markdown returns, marklassian among -them. Every cell cites that package's README or a run against its published version, since a -reader relies on it. Found while planning discoverability, 2026-10-04. +them. Every cell cites that package's README or a run against its published version, and the +table names each version and the date it was checked, since a reader relies on it. Found while planning discoverability, 2026-10-04. ### 43. Give each markdown input its own reader, strict to its own standard. @@ -150,12 +158,16 @@ npm list and the names the README uses — ADF, Atlassian Document Format, markd The maintainer's call, 2026-10-04: GitHub is where an ADF converter is searched for, and the Gitea copy keeps the move back open. The workflows move to `.github/workflows/` with `ci.sh` and -`publish.sh` unchanged; `package.json`'s `repository`, `bugs` and `homepage`, the README's, -CHANGELOG's and MIGRATION's links, and AGENTS.md's forge commands and merge style follow. +`publish.sh` unchanged. Publishing moves to npm's trusted publishing over OIDC, which GitHub-hosted +Actions support, and which outlives the bypass-2FA token npm retires around January 2027. +`package.json`'s `repository` moves and it gains `bugs` and `homepage`. The links in README, +CHANGELOG and MIGRATION follow, as do AGENTS.md's forge commands and merge style. `docs/decisions.md` gains the entry "GitHub is canonical, and Gitea a pull mirror of it" (Goal 3), -and every entry naming Gitea Actions is revised. Creating the GitHub repository, its `NPM_TOKEN` -secret, and re-creating the Gitea copy as a mirror — Gitea converts a repository to a mirror only -that way — are the maintainer's. +and every entry naming Gitea Actions is revised. Gitea's open issues and its releases move to +GitHub first, since a mirror carries only git refs; the mirror disables issues and its description +points at GitHub. Creating the GitHub repository, configuring npm's trusted publisher, and +re-creating the Gitea copy as a mirror (Gitea cannot convert an existing repository) are the +maintainer's. ### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`. @@ -168,6 +180,16 @@ item 43: `MIGRATION.md`'s Readings and Spellings tables gain their rows. Example their `pending` exceptions, and the spelling leaves the README's "Four CommonMark spellings" bullet, which counts one fewer. +### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. + +Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the +tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the +figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since +ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build +or report the unminified gzip). The figure lands in README §The package beside the "no runtime +dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 +kB. + ### 61. Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it. The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the @@ -223,19 +245,10 @@ link mark, so a link whose text is empty takes its URL as its text (the maintain `pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, which counts one fewer. -### 47. Open the README with what the package is, what it does and for whom. - -It opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being -decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why" -note left. The badges are npm's version and the Gitea Actions status. The README names the lossy -pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes and reads — GitHub -Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for -any of these names finds the package. - ### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions. -Lands after `0.2.0` is published and items 71 and 72 land, so each post links the GitHub -repository and the comparison. We draft each post for the place it goes; the maintainer posts +Lands after `0.2.0` is published with items 47, 71 and 72, so each post links the GitHub +repository, the comparison and a README that opens with what the package is. We draft each post for the place it goes; the maintainer posts them. Found while planning discoverability, 2026-10-04. ### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. @@ -243,30 +256,6 @@ them. Found while planning discoverability, 2026-10-04. `adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values already escape it. -### 48. Keep the release path publishing past npm's bypass-2FA token retirement. - -Lands after 2027-01-01, or after a release run fails on the token, whichever comes first: the -maintainer chose on 2026-10-02 to wait and see whether the retirement bites. It holds back no -release: when the rest of its release is done, it moves to the next. `0.1.0` published only once the -npm token carried **Bypass 2FA**: the account requiring no 2FA on writes was not enough, and npm -answered `EOTP` until the token itself bypassed. npm retires bypass-2FA tokens for direct publishing -around January 2027, leaving them `npm stage publish`, which a maintainer approves with 2FA; its -replacement — trusted publishing over OIDC — supports GitHub-hosted Actions, GitLab.com's shared -runners and CircleCI's cloud, self-hosted runners planned without a date. Revisit: whether npm has -added Gitea or self-hosted OIDC, and otherwise whether the release moves to the staged publish — -which fits badly with publish-on-merge, and is the maintainer's trade to weigh. The Goals and G -cells are provisional: no README goal covers the release path. - -### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. - -Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the -tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the -figure the competitors advertise (marklassian's "12kb") and the only apples-to-apples one, since -ours ships tsc's unminified output and no minifier yet (decide here whether to minify for the build -or report the unminified gzip). The figure lands in README §The package beside the "no runtime -dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 -kB. - ### 34. Read emphasis flanking by the whole character beside an astral symbol. Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an -- 2.52.0 From db4599837c22d060d08cc1ffe634f87982ff72f6 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:28:14 +0200 Subject: [PATCH 4/8] Answer prose round 1: the plain bullet ties the drop to what a reader sees, and the editor-normal comments read once --- README.md | 2 +- src/markdown/plain/editor-normal.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2f27dfa..7e250dd 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ read replaces mentions, attachments and macros with text. - A newline in text is a hard break and in an expand's title a space, edge whitespace outside a link or code span is trimmed, carriage returns and null characters are removed, and an empty paragraph drops. -- A text node's `content`, which Atlassian's schema forbids, drops. +- A text node's `content` drops: Atlassian's schema forbids it, so no rendered document shows it. - An ordered list numbered past `999999999`, or adjacent ordered lists whose numbering does not continue, is one bullet list keeping its numbers as text. - A task list beside a bullet or decision list, or holding a block other than a task, joins one diff --git a/src/markdown/plain/editor-normal.ts b/src/markdown/plain/editor-normal.ts index a5fc8ba..f9ad390 100644 --- a/src/markdown/plain/editor-normal.ts +++ b/src/markdown/plain/editor-normal.ts @@ -7,7 +7,7 @@ type JsonContainer = JsonValue[] | { [key: string]: JsonValue } type NodeHolder = { content?: AdfNode[] } -// The editor's rule is the reader's over the pair's editor-normal forms. +// The editor joins two text nodes where the reader would join their editor-normal forms. export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean { if (previous.type !== 'text' || node.type !== 'text') return false return joinsWhenRead(normalNode(previous), normalNode(node)) @@ -25,7 +25,7 @@ export function toEditorNormal(document: AdfDocument): AdfDocument { if (content.length === 0) continue entry.holder.content = content.map((source) => { const holder = normalNode(source) - // Atlassian's schema allows a text node no content, so the editor keeps none. + // Atlassian's schema forbids content on a text node, so the editor drops it. if (source.type !== 'text') pending.push({ holder, source }) return holder }) -- 2.52.0 From 730bf88da7a02a2d29285f0a248b1cfb2547c862 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:29:43 +0200 Subject: [PATCH 5/8] Answer prose round 2: item 72 names the entries it revises and the publishing change, item 47 opens with what it lands after --- todo.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/todo.md b/todo.md index c4567c7..d0802a0 100644 --- a/todo.md +++ b/todo.md @@ -63,12 +63,12 @@ ### 47. Open the README with what the package is, what it does and for whom. -It opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being +Lands after item 72: the badges are npm's version and the GitHub Actions status. The README opens +with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why" -note left. The badges are npm's version and the GitHub Actions status, so it lands after item 72. The README names the lossy -pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes and reads — GitHub -Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for -any of these names finds the package. +note left. The README names the lossy pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the +flavours it writes and reads — GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored +Markdown's callouts — so a search for any of these names finds the package. ### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF. @@ -112,11 +112,12 @@ key per element: `ok: true` with content lost. Accept an object whose prototype ### 71. Add a README table comparing the package with the other ADF converters, each cell checked against that package's own docs or behaviour. -Lands after items 7, 46 and 47: the table names HTML, compares item 46's size figure, and sits in the README item 47 reshapes. A table: which directions each converts, -whether the round-trip holds, what an unknown node does, the formats, runtime dependencies and -size. Candidates are the packages an npm search for ADF and markdown returns, marklassian among -them. Every cell cites that package's README or a run against its published version, and the -table names each version and the date it was checked, since a reader relies on it. Found while planning discoverability, 2026-10-04. +Lands after items 7, 46 and 47: the table names HTML, compares item 46's size figure, and sits in +the README item 47 reshapes. Its columns: which directions each converts, whether the round-trip +holds, what an unknown node does, the formats, runtime dependencies and size. Candidates are the +packages an npm search for ADF and markdown returns, marklassian among them. Every cell cites that +package's README or a run against its published version, and the table names each version and the +date it was checked. Found while planning discoverability, 2026-10-04. ### 43. Give each markdown input its own reader, strict to its own standard. @@ -157,17 +158,18 @@ npm list and the names the README uses — ADF, Atlassian Document Format, markd ### 72. Make GitHub the canonical repository, with `gitea.larvit.se/larvit/adf-codec` a read-only pull mirror of it. The maintainer's call, 2026-10-04: GitHub is where an ADF converter is searched for, and the Gitea -copy keeps the move back open. The workflows move to `.github/workflows/` with `ci.sh` and -`publish.sh` unchanged. Publishing moves to npm's trusted publishing over OIDC, which GitHub-hosted -Actions support, and which outlives the bypass-2FA token npm retires around January 2027. -`package.json`'s `repository` moves and it gains `bugs` and `homepage`. The links in README, -CHANGELOG and MIGRATION follow, as do AGENTS.md's forge commands and merge style. +copy keeps the move back open. The workflows move to `.github/workflows/` with `ci.sh` unchanged. +Publishing moves to npm's trusted publishing over OIDC, which GitHub-hosted Actions support, and +which outlives the bypass-2FA token npm retires around January 2027: `publish.sh` drops `NPM_TOKEN` +for the Actions OIDC variables, and AGENTS.md §7 reserves the trusted-publisher setup in place of +the secret. `package.json`'s `repository` moves and it gains `bugs` and `homepage`. The links in +README, CHANGELOG and MIGRATION follow, as do AGENTS.md's forge commands and merge style. `docs/decisions.md` gains the entry "GitHub is canonical, and Gitea a pull mirror of it" (Goal 3), -and every entry naming Gitea Actions is revised. Gitea's open issues and its releases move to -GitHub first, since a mirror carries only git refs; the mirror disables issues and its description -points at GitHub. Creating the GitHub repository, configuring npm's trusted publisher, and -re-creating the Gitea copy as a mirror (Gitea cannot convert an existing repository) are the -maintainer's. +and §Public on npm and §Publish on a version bump are revised: the source is public on GitHub, and +CI publishes through npm's trusted publisher. Gitea's open issues and its releases move to GitHub +first, since a mirror carries only git refs; the mirror disables issues and its description points +at GitHub. Creating the GitHub repository, configuring npm's trusted publisher, and re-creating the +Gitea copy as a mirror (Gitea cannot convert an existing repository) are the maintainer's. ### 49. Read a list whose bullet or ordered delimiter changes as two lists, in both readers, and retire `!adf:listBreak`. @@ -247,9 +249,9 @@ which counts one fewer. ### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions. -Lands after `0.2.0` is published with items 47, 71 and 72, so each post links the GitHub -repository, the comparison and a README that opens with what the package is. We draft each post for the place it goes; the maintainer posts -them. Found while planning discoverability, 2026-10-04. +Lands after `0.2.0` is published with items 47, 71 and 72, so each post links the GitHub repository, +the comparison and a README that opens with what the package is. We draft each post for the place it +goes; the maintainer posts them. Found while planning discoverability, 2026-10-04. ### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. -- 2.52.0 From 8a27cb52d865b01d7e5d0afdeb723217ac85d640 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:29:43 +0200 Subject: [PATCH 6/8] Answer product-owner round 2: item 72 drops the npm token for the trusted publisher -- 2.52.0 From 55c6f01517097593bf4e65e583f071d9d3a02217 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:29:53 +0200 Subject: [PATCH 7/8] Answer prose round 2: item 47 marks the README's current opening as today's --- todo.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/todo.md b/todo.md index d0802a0..cd35449 100644 --- a/todo.md +++ b/todo.md @@ -63,8 +63,8 @@ ### 47. Open the README with what the package is, what it does and for whom. -Lands after item 72: the badges are npm's version and the GitHub Actions status. The README opens -with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being +Lands after item 72: the badges are npm's version and the GitHub Actions status. Today the README +opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being decommissioned, a link to JRACLOUD-77436. The background goes entirely, no endpoint, ticket or "why" note left. The README names the lossy pair, `adfToPlainMarkdown` and `plainMarkdownToAdf`, and the flavours it writes and reads — GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored -- 2.52.0 From 5798a3cde1c21c80f9254cd1cf73ee836e9add90 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sun, 4 Oct 2026 22:32:07 +0200 Subject: [PATCH 8/8] Answer prose round 3: clean -- 2.52.0