329 Commits

Author SHA1 Message Date
lilleman 31d3f53a58 Leave Flavour's move to item 59 alone
CI / gate (push) Successful in 52s
CI / publish (push) Successful in 5s
2026-10-03 17:54:41 +02:00
lilleman 80a45b69da Sharpen items 55, 62, 63, 64 and 65 2026-10-03 17:53:30 +02:00
lilleman 8287a483db Take round 5 as the comprehension baseline, and tighten four decision sentences 2026-10-03 17:53:13 +02:00
lilleman 7b38eba29e Reword the isBareText and adfNodes comments 2026-10-03 17:52:56 +02:00
lilleman ee9a7e7562 Join only content-free text nodes in editor-normal ADF, type checked first: adfToPlainMarkdown on 3,000 extension blocks of 200-key parameters 1,752-1,941 ms → 1,034-1,240 ms (1,018-1,283 ms at 608ef26) 2026-10-03 17:52:52 +02:00
lilleman 317262b240 Say which emitter refuses which text node in item 55 2026-10-03 17:02:57 +02:00
lilleman eccfd12d2d Cut the empty-key and -0 grammar from the decisions into the spec, and cite item 65 as what ends the image gap
CI / gate (push) Successful in 52s
CI / publish (push) Has been skipped
2026-10-03 16:23:23 +02:00
lilleman 7bab478729 Keep an empty doc content in editor-normal ADF, as the schema requires 2026-10-03 16:22:54 +02:00
lilleman 00c89dd63b Give the unmarked bare text check and the text-holding-content refusal one owner each 2026-10-03 16:22:19 +02:00
lilleman fd3452b124 Name the reader's own-marks items for what they hold, and drop the carry flag the items already say 2026-10-03 16:21:49 +02:00
lilleman d56e5b4f57 Keep near-miss text pairs apart in the property harness: merge only what the reader joins 2026-10-03 16:21:06 +02:00
lilleman 36604b1e2b Hold the editor's join rule in plain/editor-normal.ts as the reader's over editor-normal forms 2026-10-03 16:19:55 +02:00
lilleman 1791542b3c Rename isPlainText to isBareText, apart from the plain flavour, and say truly what it admits 2026-10-03 16:19:09 +02:00
lilleman f7d2eb6517 Widen item 56 to the inline scanner's Scan 2026-10-03 16:18:30 +02:00
lilleman 79005f07be File the round-4 goals audit's defects and widen items 55 and 62 2026-10-03 16:16:46 +02:00
lilleman 7f67998c3b Prose round 3: retitle the floor decision, put the plain/ exception first, sharpen items 59-62
CI / gate (push) Successful in 57s
CI / publish (push) Has been skipped
2026-10-03 16:11:56 +02:00
lilleman b39525cad7 Ship under the comprehension floor with the panel's scores as the baseline, and file items 59-62 that end it
CI / gate (push) Successful in 52s
CI / publish (push) Has been skipped
2026-10-03 15:53:25 +02:00
lilleman d0cb63fcf2 Name the emitter's block walk PlacedBlocks, apart from the parser's Walk 2026-10-03 15:52:43 +02:00
lilleman 1d3a020926 Keep 'carry' for the opaque carry: holdsOnlyAttributes, spellEdgeWhitespace, bothCommonMark 2026-10-03 15:52:19 +02:00
lilleman 4579f32fcd Name the two text-joining rules for whose rule they are, side by side in adjacent-text.ts 2026-10-03 15:51:41 +02:00
lilleman 6df17a7069 Gather the plain flavour's code in markdown/plain/ 2026-10-03 15:50:24 +02:00
lilleman 4349df9017 Final wording: the reserved-directive refusal row, the adf:-alone fence, the changelog's carry and scope, the developer panel, the migration's empty shape 2026-10-03 15:17:53 +02:00
lilleman d68f311a14 Reword the attrs=empty and language-placement refusals, quoting a language that needs it 2026-10-03 15:17:40 +02:00
lilleman 608ef266ed Reword item 53's tagline
CI / gate (push) Successful in 56s
CI / publish (push) Has been skipped
2026-10-03 15:13:57 +02:00
lilleman a2c620da27 Define the writer panel, split the carry fence into its own changelog entry, and say how an adf: fence stays code 2026-10-03 15:13:50 +02:00
lilleman e4eecebeab Keep the decisions to their reasons and link the grammar; name the bare adf: fence's refusal exactly; call the doc panel a writer panel 2026-10-03 15:13:30 +02:00
lilleman 230d590fb4 Trim two comments 2026-10-03 15:12:54 +02:00
lilleman 7846502a1a Lead the code block, empty-key, carry fence, text break and carry-in-mark refusals with their fix 2026-10-03 15:12:51 +02:00
lilleman 79d9b4db53 Keep the fence wrapper's code span on one line
CI / gate (push) Successful in 52s
CI / publish (push) Has been skipped
2026-10-03 15:06:22 +02:00
lilleman 912c5039aa Rewrap the broken lines, and say plainly how an adf: fence stays code 2026-10-03 15:06:11 +02:00
lilleman fb78063f12 Name the code block wrapper in the two carry-fence refusals that lacked it 2026-10-03 15:05:51 +02:00
lilleman 5cffe82dd5 Lead the refusals ordinary editing meets with the edit that fixes them 2026-10-03 15:05:36 +02:00
lilleman d9f2d325ca Tell migrators an empty stored document gets content: [], and what an adf: fence refuses 2026-10-03 15:04:50 +02:00
lilleman 419bbcb4f2 Say equality is deep over the document's JSON values 2026-10-03 15:04:44 +02:00
lilleman 4d7e8c944c Pin a code block holding content=empty beside its language 2026-10-03 15:04:35 +02:00
lilleman ff9bb7fab2 Inline the identity blocks() helper in the plain reader tests 2026-10-03 15:04:15 +02:00
lilleman 5a7279059d Drop the as const on the doc directive's attribute 2026-10-03 15:04:11 +02:00
lilleman 0ac5dccf11 Pin the empty fence beside another as an error fixture 2026-10-03 15:04:04 +02:00
lilleman 6fc0d6bba9 Refuse attrs=empty on a code block whose fences carry its language 2026-10-03 15:03:58 +02:00
lilleman 423dcbbff5 File the audits' pre-existing findings, and mark item 43 cited by decisions
CI / gate (push) Successful in 53s
CI / publish (push) Has been skipped
2026-10-03 14:47:57 +02:00
lilleman b9760d0af2 Name plainMarkdownToAdf in the empty-markdown changelog entry
CI / gate (push) Successful in 52s
CI / publish (push) Has been skipped
2026-10-03 14:46:29 +02:00
lilleman d7ba8e6aae Keep format knowledge out of mergeAdjacentText's comment 2026-10-03 14:46:29 +02:00
lilleman bba2fa409e Drop empty keys in the property harness through emptyKeys 2026-10-03 14:46:29 +02:00
lilleman c3bc2eb572 Read a carry fence's type in one function 2026-10-03 14:45:48 +02:00
lilleman 2700184e56 Spell the doc directive's content=none through spellAttributes from one constant 2026-10-03 14:45:28 +02:00
lilleman d9c3e4b349 Derive a block directive's empty content from its node, and let readEmptyKeys own the attrs=empty-stands-alone rule 2026-10-03 14:45:12 +02:00
lilleman 2d02165702 Keep the browser leg on dist/index.js: tag -0 in the JSON it hands back 2026-10-03 14:44:14 +02:00
lilleman a565d49c24 Spell 'a text node holding its text and marks alone' once, as isPlainText 2026-10-03 14:44:00 +02:00
lilleman ade3997cfd Hold a carried node's own marks on the item, not in a side set copies fall out of 2026-10-03 14:43:03 +02:00
lilleman 31576bb824 Take the raw run's refusal path only where its code matches the editor-normal refusal 2026-10-03 14:41:42 +02:00
lilleman 723c0e31ef Name the adf: fence among the carve-outs, and the way a fence stays code in its errors 2026-10-03 14:41:30 +02:00
lilleman a5238e4fe5 Cite item 43 as what ends the CommonMark carve-outs and the adf: fence reservation 2026-10-03 14:40:31 +02:00
lilleman 9f0f520afd Give the comprehension panel its fill-ins
CI / gate (push) Successful in 52s
CI / publish (push) Has been skipped
2026-10-03 14:34:28 +02:00
lilleman 45f1bee94b Set up every inline scan through one freshScan
CI / gate (push) Successful in 53s
CI / publish (push) Has been skipped
2026-10-03 14:33:06 +02:00
lilleman 20a191e7e9 Write the same plain markdown for documents the editor holds equal: normalize the reduction's input again, naming a refusal by the caller's document 2026-10-03 14:32:34 +02:00
lilleman 3da45a9da7 File item 54: the source-parting decision misses an import parse/ takes from emit/
CI / gate (push) Successful in 45s
CI / publish (push) Has been skipped
2026-10-03 14:23:13 +02:00
lilleman 0bc19238b4 Say only the plain writer is editor-normal, and read plain markdown in the tests without normalizing 2026-10-03 14:23:04 +02:00
lilleman 1458b09449 Index a plain refusal's path in the caller's document: normalize only the reduction's output 2026-10-03 14:22:14 +02:00
lilleman eab28ae8ff Refuse a carry fence naming a type no info string carries back 2026-10-03 14:20:47 +02:00
lilleman afb53ca594 Keep an empty stored document empty in the migration recipe 2026-10-03 14:20:03 +02:00
lilleman 70e2639982 Carry the text break as its own item, not a fake node, and check it where the outermost content resolves 2026-10-03 14:19:55 +02:00
lilleman fdbf43398c Keep one merge loop and one mark key: editor-normal compares normalized marks with identicalMark 2026-10-03 14:17:22 +02:00
lilleman 0c507f981f Name the writer panel behind the doc spelling
CI / gate (push) Successful in 1m46s
CI / publish (push) Has been skipped
2026-10-03 12:31:49 +02:00
lilleman 1d0ae695da Name the code block the carry now holds in the migration's error codes 2026-10-03 12:31:10 +02:00
lilleman f180943edf Record deep equality, the text break, empty keys, -0, the typed carry fence and per-node code fences; file items 52 and 53 2026-10-03 12:28:20 +02:00
lilleman 9ab7286e6d Make markdownToAdf(adfToMarkdown(doc)) deep-equal doc: textBreak, empty keys, -0, typed carry fences, code blocks of several text nodes 2026-10-03 12:25:01 +02:00
lilleman bfc4b2ce00 Name the markdown's readers and writers in the audience, and the panel rule 2026-10-03 12:03:20 +02:00
lilleman 6f689a51b0 Answer the prose review's third round
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 5s
2026-10-02 23:20:49 +02:00
lilleman ddc5dfa1be Answer the prose and product-owner reviews' second round
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-10-02 23:10:23 +02:00
lilleman 6c589ecabc Answer the prose and product-owner reviews' first round
CI / gate (push) Successful in 43s
CI / publish (push) Has been cancelled
2026-10-02 23:08:47 +02:00
lilleman da63faa4a4 File the three pending CommonMark gaps, hold item 48 until 2027, and give item 8 its own goal and persona
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-10-02 23:02:05 +02:00
lilleman d0f0873ca9 Answer the prose review's first round
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 5s
2026-10-02 22:46:15 +02:00
lilleman 058a5fd2f8 Drop the retired milestone wording from the corpus README
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-10-02 22:45:03 +02:00
lilleman e84cd47f08 Answer the prose review's third round
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 6s
2026-10-02 22:38:44 +02:00
lilleman 7adab8e19e Answer the prose review's second round
CI / gate (push) Successful in 43s
CI / publish (push) Waiting to run
2026-10-02 22:37:59 +02:00
lilleman dff4983d4a Answer the prose and product-owner reviews' first round
CI / gate (push) Successful in 43s
CI / publish (push) Has been skipped
2026-10-02 22:36:37 +02:00
lilleman d9bacc4072 Give the README's HTML documentation to item 7
CI / gate (push) Successful in 48s
CI / publish (push) Has been skipped
2026-10-02 22:32:29 +02:00
lilleman 58a7bf91b0 Move items 31, 33, 34, 38, 42, 46 and 47 to 0.3.0 2026-10-02 22:30:58 +02:00
lilleman 91adec5797 Score and rank todo.md in one table, and leave the empty-release and weighing rules to the global loop 2026-10-02 22:17:03 +02:00
lilleman 2252232fd6 Goals judged in order, Goal 7 names no runtime dependencies, file the isAdfDocument result
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 5s
2026-10-02 21:57:03 +02:00
lilleman 4d3231c86b Answer the product-owner review's third round
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-10-02 17:41:54 +02:00
lilleman 95af770e0a Answer the prose review's third round
CI / gate (push) Successful in 40s
CI / publish (push) Has been skipped
2026-10-02 17:41:47 +02:00
lilleman 361139a4d4 Rewrap two lines past 100 columns
CI / gate (push) Successful in 40s
CI / publish (push) Has been cancelled
2026-10-02 17:41:07 +02:00
lilleman dccd8dcf3a Answer the prose review's second round and the product-owner review's second
CI / gate (push) Successful in 40s
CI / publish (push) Has been skipped
2026-10-02 17:41:00 +02:00
lilleman cd2b573372 Answer the prose and product-owner reviews' first round
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-10-02 17:39:25 +02:00
lilleman 94eaee6bae Goals become one-line aims, their detail moves to the sections that meet them
CI / gate (push) Successful in 1m43s
CI / publish (push) Has been skipped
2026-10-02 17:34:17 +02:00
lilleman e949f2099c review: the notes bullet states when each note fires, syncBlock named
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 6s
2026-09-30 22:30:09 +02:00
lilleman 32df104649 review: Goal 5 keeps content the document holds, the notes bullet names when each fires
CI / gate (push) Successful in 44s
CI / publish (push) Has been cancelled
2026-09-30 22:29:38 +02:00
lilleman 1df58cf33e Goal 5's omission-note sentence moves to the plain flavour's spellings decision
CI / gate (push) Successful in 40s
CI / publish (push) Has been cancelled
2026-09-30 22:28:40 +02:00
lilleman e17e247351 review: Goal 5 names the round trip and drops the minted-ids clause
CI / gate (push) Successful in 45s
CI / publish (push) Successful in 6s
2026-09-30 20:01:29 +02:00
lilleman 5f3705c430 Goal 5 states that a document's identity survives only the lossless pair
CI / gate (push) Successful in 47s
CI / publish (push) Has been cancelled
2026-09-30 20:00:47 +02:00
lilleman 7b344334b6 41 - review: Goal 6 breaks ties only, the mailto: rule stated plainly
CI / gate (push) Successful in 43s
CI / publish (push) Successful in 5s
2026-09-30 19:49:40 +02:00
lilleman 8d9d1ecb5b 41 - review: title text keeps its own spelling, only a node without text reads as the reduction does
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-30 17:01:15 +02:00
lilleman 3f2f63aac0 41 - review: a callout title reads its nodes as the plain reduction does, one goal reference renumbered
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-30 16:57:54 +02:00
lilleman 8cecf27757 41 - plainMarkdownToAdf keeps a callout title's link targets; Goal 6, what happens is what the audience expects
CI / gate (push) Successful in 1m44s
CI / publish (push) Has been skipped
2026-09-30 16:32:13 +02:00
lilleman 1f7d11ea3e 10f - review: the position id and carry entries and the errors intro stated truly
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-29 23:37:39 +02:00
lilleman 050e52296a 10f - review: README says to read markdown bound for one document once
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-29 23:35:24 +02:00
lilleman bd8d240712 10f - review: a node the carry restores stays deep-equal, its ids only skipped
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-29 21:51:22 +02:00
lilleman 3edc12aaa9 10f - review: tests for a carried node keeping its task nodes unminted 2026-09-29 21:51:22 +02:00
lilleman c81bda9bfb 10f - plainMarkdownToAdf mints task localIds as UUID v4s hashed from the markdown and position
CI / gate (push) Successful in 44s
CI / publish (push) Has been skipped
2026-09-29 21:47:09 +02:00
lilleman 160c2a7052 cjk - review: the voicing marks' script stated truly, one spelling for the alphabet
CI / gate (push) Successful in 46s
CI / publish (push) Successful in 6s
2026-09-29 18:52:03 +02:00
lilleman e2a660ee8e cjk - review: Hangul, the long-vowel mark and a character inside a delimiter bound a highlight
CI / gate (push) Successful in 44s
CI / publish (push) Has been cancelled
2026-09-29 18:49:55 +02:00
lilleman f105bc9a57 cjk - review: tests for Hangul, a long-vowel mark and a Latin word bounding a highlight 2026-09-29 18:49:37 +02:00
lilleman 6c499127b5 cjk - a letter of a script written without spaces bounds a highlight
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-29 18:43:29 +02:00
lilleman 151bf38398 cjk - tests for a highlight flush against a script written without spaces 2026-09-29 18:43:04 +02:00
lilleman a4b6b48635 35b - review: README names the marker escapes, a spec line reflowed
CI / gate (push) Successful in 1m39s
CI / publish (push) Successful in 6s
2026-09-29 12:23:16 +02:00
lilleman 1a266d5661 35b - review: the escape test names the renderer
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-29 12:21:51 +02:00
lilleman 9bdbcd880e 35b - review: README names when a task list keeps its states as text
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-29 12:20:46 +02:00
lilleman c3e86f8430 35b - review: escape a task marker opening any list item's first paragraph
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-29 12:18:21 +02:00
lilleman 2395cf077e 35b - review: tests for escaping every list item's task marker 2026-09-29 12:18:16 +02:00
lilleman 95ff9ee910 35b - review: a task-list child reducing to nothing counts toward neither spelling
CI / gate (push) Successful in 44s
CI / publish (push) Has been cancelled
2026-09-29 12:13:54 +02:00
lilleman 6681bec391 35b - review: runs as a non-empty tuple, no item for a stray child reducing to nothing, 42 filed 2026-09-29 12:13:11 +02:00
lilleman 6c8ed03d36 35b - review: test that a stray task-list child reducing to nothing writes no item 2026-09-29 12:13:00 +02:00
lilleman 43869a7219 35b - review: drop every unread highlight in one pass, one task-as-text path, a linear title trim
CI / gate (push) Successful in 44s
CI / publish (push) Has been cancelled
2026-09-29 12:05:04 +02:00
lilleman 33f3bf7ee4 35b - review: tests for dropping every unread highlight, a phantom task item and a title's trailing blanks 2026-09-29 12:01:56 +02:00
lilleman 1d369ec5c5 35b - a leading marker reads two characters past it
CI / gate (push) Successful in 45s
CI / publish (push) Has been cancelled
2026-09-29 11:45:15 +02:00
lilleman 52cf681a1c 35b - 35 done 2026-09-29 11:42:50 +02:00
lilleman 06103ea2ba 35b - the writer spells the plain flavour and escapes text that would read as its markers 2026-09-29 11:42:46 +02:00
lilleman 7c15b02258 35b - tests for the writer spelling the plain flavour and escaping its markers 2026-09-29 11:32:45 +02:00
lilleman 692433b712 41 - a callout title keeps its link targets, decided
CI / gate (push) Successful in 41s
CI / publish (push) Has been cancelled
2026-09-29 10:55:01 +02:00
lilleman b9e2842310 35a - review: prose pass on README §Plain markdown and §message and path
CI / gate (push) Successful in 43s
CI / publish (push) Successful in 6s
2026-09-28 22:21:28 +02:00
lilleman 75c5917712 35a - review: an image refusal beside a marker names the line it shares, README wording, 41 filed
CI / gate (push) Successful in 43s
CI / publish (push) Has been skipped
2026-09-28 22:17:22 +02:00
lilleman daaa2134b3 35a - review: tests for the image refusal naming where the image sits 2026-09-28 22:17:22 +02:00
lilleman af7658a863 35a - review: a title keeps its ==, an image beside a marker is refused as before
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-28 22:12:58 +02:00
lilleman 13b04e46d3 35a - review: tests for an expand title's == and an image beside a marker 2026-09-28 22:12:58 +02:00
lilleman 474e6a2991 35a - the parser reads the plain flavour and the lift goes
CI / gate (push) Successful in 43s
CI / publish (push) Has been skipped
2026-09-28 22:02:55 +02:00
lilleman 23804a5b3a 40 - plan exact deep-equality for the round-trip, replacing 39
CI / gate (push) Successful in 43s
CI / publish (push) Successful in 7s
2026-09-28 21:39:47 +02:00
lilleman 8d2d2405f3 Goal 7 names the public surface; 39 exports sameAdf
CI / gate (push) Successful in 44s
CI / publish (push) Successful in 7s
2026-09-28 21:25:19 +02:00
lilleman 4443ea57af 36e - todo-history.md's decisions move to docs/decisions.md and it goes
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 7s
2026-09-28 20:59:22 +02:00
lilleman 09d2e54f5a 36d - review: foreign HTML's goals and the loss it names, entries that land with their items
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 7s
2026-09-28 18:56:50 +02:00
lilleman 2701e165c4 36d - todo.md's settled text and §11 and §15's dated rules move to docs/decisions.md
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-28 18:55:09 +02:00
lilleman 2a3c90d63b 37 - Deno stays to prove the library runs there
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 7s
2026-09-28 18:45:09 +02:00
lilleman bbfc8724fc 36c - review: a Deno premise that holds today, the plain reduction's memo named
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-28 11:14:27 +02:00
lilleman fa6df33987 36c - review: premises that expire, no todo-history citations, the memo's key at the code, Goal 10 checkable
CI / gate (push) Successful in 43s
CI / publish (push) Has been skipped
2026-09-28 11:08:17 +02:00
lilleman 8a898436ea 36c - Goal 10's continuation indent
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-28 11:00:42 +02:00
lilleman 621dc75f86 36c - Goal 10: source a contributor can hold
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-28 11:00:39 +02:00
lilleman d5dae87c9b 36c - §10 and §11's decisions move to docs/decisions.md
CI / gate (push) Successful in 43s
CI / publish (push) Has been cancelled
2026-09-28 10:55:07 +02:00
lilleman 348865398c 36 - review: goal 8 covers what other parsers read, goal 1's tagline names both halves
CI / gate (push) Successful in 43s
CI / publish (push) Successful in 7s
2026-09-28 10:41:49 +02:00
lilleman ff23145a5a 36 - Goal 1 holds that every call answers, Goal 8 says what correct means
CI / gate (push) Successful in 43s
CI / publish (push) Has been cancelled
2026-09-28 10:40:48 +02:00
lilleman d2a3d74030 36b - Goals 8 and 9: correct before fast, fast once correct
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 7s
2026-09-28 10:22:08 +02:00
lilleman 50f97513e8 36b - review: publish entry matches publish.sh
CI / gate (push) Successful in 1m3s
CI / publish (push) Has been skipped
2026-09-28 03:56:16 +02:00
lilleman e5771bc60b 36b - review: no stale private flag, no undefined freeze
CI / gate (push) Successful in 43s
CI / publish (push) Has been cancelled
2026-09-28 03:55:29 +02:00
lilleman fb97285899 36b - §8, §9 and §14's decisions move to docs/decisions.md
CI / gate (push) Successful in 42s
CI / publish (push) Has been cancelled
2026-09-28 03:54:34 +02:00
lilleman c64eed9301 36 - review: one spelling for public npm, a premise the goal does not restate
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-28 02:45:20 +02:00
lilleman d10da170b1 36 - Goal 7 names public npm, the Public on npm entry cites it
CI / gate (push) Successful in 46s
CI / publish (push) Has been skipped
2026-09-28 02:35:09 +02:00
lilleman c694896090 36a - review: README links the decision log, premises that can lapse, citations fixed
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-28 00:39:05 +02:00
lilleman becd12e294 36a - §1–§6's decisions move to docs/decisions.md, indexed from AGENTS.md
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-28 00:37:01 +02:00
lilleman f855e1b348 36 - review: wrap
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-28 00:04:44 +02:00
lilleman c6781aa9b3 36 - review: CHANGELOG flags the breaking readings and codes, README links it
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-28 00:04:38 +02:00
lilleman 740a7b1e59 36 - plan: todo.md by release, CHANGELOG.md, done items leave todo.md
CI / gate (push) Successful in 42s
CI / publish (push) Has been skipped
2026-09-28 00:03:37 +02:00
lilleman a74e8742ff 35 - todo taglines state the target
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 5s
2026-09-27 23:41:46 +02:00
lilleman 11e55732e7 35 - review: the error table names the lossless flavour
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-27 23:32:09 +02:00
lilleman 8e09cd2214 35 - review: the README names the lossless and plain flavours
CI / gate (push) Successful in 41s
CI / publish (push) Has been cancelled
2026-09-27 23:31:51 +02:00
lilleman 5d5952c576 35 - goals: ADF is the hub, plain markdown planned as a flavour of the grammar
CI / gate (push) Successful in 1m39s
CI / publish (push) Has been cancelled
2026-09-27 23:30:35 +02:00
lilleman 1271365b38 10 - decisions: the lossy pair never saves back, a callout's title is its marker line, task ids by position, 5g names the flavours
CI / gate (push) Successful in 42s
CI / publish (push) Successful in 6s
2026-09-26 14:45:29 +02:00
lilleman 1f4a94974a 10c - prose: the lossy pair folds into §1, §15's ask rule said once
CI / gate (push) Successful in 41s
CI / publish (push) Successful in 6s
2026-09-26 13:33:36 +02:00
lilleman 326352fa98 10c - review: README says which pair saves back, where plainMarkdownToAdf refuses, and the localId the schema wants
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-26 13:32:25 +02:00
lilleman 9e2c0fda15 10c - a list standing in a list or task list keeps its numbers as text past the marker cap
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-26 13:28:13 +02:00
lilleman f594dafc5b 10c - review: link and code span edges named in the README, 10e filed, leafEdges trail simplified
CI / gate (push) Successful in 40s
CI / publish (push) Has been skipped
2026-09-26 13:26:20 +02:00
lilleman 613dc0edbb 10c - adfToPlainMarkdown and plainMarkdownToAdf exported, whitespace-only leaves trimmed at a line end
CI / gate (push) Successful in 41s
CI / publish (push) Has been skipped
2026-09-26 13:20:16 +02:00
lilleman 2f03e20549 10b - 34 files the flanking read beside an astral symbol
CI / gate (push) Successful in 34s
CI / publish (push) Successful in 7s
2026-09-25 21:10:23 +02:00
lilleman ae2dc6054b 10b - a == pair is bounded by the code point outside it, CommonMark's word character test 2026-09-25 21:10:05 +02:00
lilleman 7a53b6f7a0 10b - tests: a symbol outside the BMP bounds a == pair 2026-09-25 21:09:12 +02:00
lilleman 1eb7b54f19 10b - item 10's highlight row needs the pair bounded outside
CI / gate (push) Successful in 35s
CI / publish (push) Has been skipped
2026-09-25 21:06:23 +02:00
lilleman 4df17055ef 10b - an empty alert holds an empty paragraph, a == pair is bounded outside and passes over held nodes 2026-09-25 21:05:58 +02:00
lilleman 8cb85f3d27 10b - tests: an empty alert holds an empty paragraph, a == pair is bounded outside and passes over held nodes 2026-09-25 21:04:50 +02:00
lilleman 969326b776 10b - 10d names the highlight that reads back wrong 2026-09-25 21:03:09 +02:00
lilleman b42218e655 10b - error reads as an error alert, 10d files the literal marker, and 10b is done
CI / gate (push) Successful in 35s
CI / publish (push) Has been skipped
2026-09-25 20:55:00 +02:00
lilleman c1fed0885b 10b - a code span inside a == pair keeps code and takes no highlight 2026-09-25 20:54:46 +02:00
lilleman bf87e6ea18 10b - tests: a code span inside a == pair takes no highlight 2026-09-25 20:54:04 +02:00
lilleman 53ff7e0204 10b - the lift, with alert words, task markers and == shared with the reduction 2026-09-25 20:52:44 +02:00
lilleman d1a208146f 10b - tests: the lift reads alerts, folded callouts, task markers and == pairs back into their nodes 2026-09-25 20:52:44 +02:00
lilleman 687ba9bf90 10a - three comments restating their function go
CI / gate (push) Successful in 35s
CI / publish (push) Successful in 6s
2026-09-25 19:40:27 +02:00
lilleman 0094b361ab 10a - item 10's list row keeps a broken numbering's numbers
CI / gate (push) Successful in 34s
CI / publish (push) Has been cancelled
2026-09-25 19:40:13 +02:00
lilleman dbd80b98c3 10a - numbered lists whose numbering breaks merge as one bullet list keeping their numbers 2026-09-25 19:39:54 +02:00
lilleman 8572d76ee9 10a - tests: a marker-shaped number opening an item stays escaped 2026-09-25 19:39:16 +02:00
lilleman 62ece3f9ac 10a - tests: numbered lists whose numbering breaks keep their numbers as text 2026-09-25 19:38:46 +02:00
lilleman 8f33f67f72 10a - a value blank once cleaned counts as empty 2026-09-25 19:37:21 +02:00
lilleman 3a18627389 10a - tests: a value blank once cleaned leaves the note or fallback 2026-09-25 19:36:59 +02:00
lilleman dc11daa540 10a - a list a numbered list becomes merges with its neighbours, and a note's name is cleaned before it is checked
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-25 19:33:26 +02:00
lilleman 7b814e02dc 10a - tests: the give-way case spells its nested list tight 2026-09-25 19:32:45 +02:00
lilleman 5e55ce2a8e 10a - tests: an overflowing numbered list merges with its neighbours, and a blank key names an extension 2026-09-25 19:32:13 +02:00
lilleman cf9a737e71 10a - 10c's property holds the plain output to no directive 2026-09-25 19:31:59 +02:00
lilleman 1c0a6526b1 10a - item 10's list row keeps a long list's numbers, and 33 names the reduction's retry
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-25 19:29:12 +02:00
lilleman 8418150cd3 10a - notes clean their names, leading rules give way, an overflowing numbered list keeps its numbers, one fallback order 2026-09-25 19:28:53 +02:00
lilleman d35637c8fa 10a - tests: a marker-shaped number opening an item stays escaped 2026-09-25 19:28:53 +02:00
lilleman c46b8243ba 10a - tests: clean note names, drop every leading rule, keep an overflowing numbered list, random localIds 2026-09-25 19:27:40 +02:00
lilleman 69f116db1b 10a - item 10's rows carry the panels' verdicts, and 10a is done
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-25 19:14:05 +02:00
lilleman 1104c57c0e 10a - a rule opening a list item gives way, and the list stays a list 2026-09-25 19:13:26 +02:00
lilleman c6acef081f 10a - the reduction keeps what a reader sees or follows, and names what the document only references 2026-09-25 19:10:47 +02:00
lilleman 59a37d1945 10a - the reduction spells a document in the flavour without directives 2026-09-25 19:03:10 +02:00
lilleman 37abc1ddc1 10a - the line emitter names the first fallback a plain line would take, and the mark run it drops 2026-09-25 19:03:10 +02:00
lilleman cb30512a90 10 - goal 1 wraps
CI / gate (push) Successful in 33s
CI / publish (push) Successful in 5s
2026-09-25 18:49:38 +02:00
lilleman 3fce43e884 10 - referenced content leaves a note naming it
CI / gate (push) Successful in 1m30s
CI / publish (push) Has been cancelled
2026-09-25 18:49:19 +02:00
lilleman 0644d1bc5d 10 - lossy conversion keeps the content, and a reader panel settles what the audience expects 2026-09-25 08:14:57 +02:00
lilleman b1dc6a1f45 27 - the level the list's directive form spends is stated at the check that pays it
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 5s
2026-09-24 00:19:45 +02:00
lilleman 0fd412e426 27 - directiveItems loses the comment that explained the removed subtraction
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-24 00:19:05 +02:00
lilleman 3d0f82cbd4 27 - the history keeps the item as written
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-24 00:14:46 +02:00
lilleman 68942c9919 27 - a placed block carries no headroom, so the list's directive items write none
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-24 00:05:08 +02:00
lilleman d5370b126d 26 - the container stack owns the directive depths, and the escape claim hands over after it records
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 5s
2026-09-24 00:02:58 +02:00
lilleman 15fb7fca81 26 - the escape phases and the container stack hold their invariants in types
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-23 23:48:08 +02:00
lilleman b90baa257c 25 - the definitions §11 held sit at the code, and §8 says only what the README does not
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 5s
2026-09-23 23:36:24 +02:00
lilleman d0088fc922 25 - AGENTS.md §8 and §11 have sub-headings and say only what the code cannot
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-23 23:29:38 +02:00
lilleman 2b15530a57 24 - the history line says what src/ root holds
CI / gate (push) Successful in 1m46s
CI / publish (push) Successful in 5s
2026-09-23 23:24:09 +02:00
lilleman 132f9f7467 24 - the conformance gates live in src/conformance/
CI / gate (push) Successful in 39s
CI / publish (push) Has been skipped
2026-09-23 23:21:06 +02:00
lilleman 7be4f59f0b 23 - the block directive's spellings are one file
CI / publish (push) Waiting to run
CI / gate (push) Successful in 39s
2026-09-23 23:13:01 +02:00
lilleman d3a2129967 32 - §8's depth sentences say what the guard does, and item 32 moves to history
CI / gate (push) Successful in 37s
CI / publish (push) Successful in 5s
2026-09-23 22:52:26 +02:00
lilleman 6715c97599 32 - the deeper helpers read the one limit, and the code directive's refusal is tested
CI / gate (push) Successful in 31s
CI / publish (push) Has been skipped
2026-09-23 22:51:03 +02:00
lilleman 75791783d8 32 - a mark's attribute depth is counted from its value, the marks spelling refusing its own
CI / gate (push) Successful in 29s
CI / publish (push) Has been skipped
2026-09-22 21:51:47 +02:00
lilleman 6944d505f1 22 - the seam is a rule in the consultation sentence, not a list
CI / gate (push) Successful in 29s
CI / publish (push) Waiting to run
2026-09-22 21:37:18 +02:00
lilleman 754f1e3b33 22 - the seam's asks bring the types their signatures name
CI / gate (push) Successful in 30s
CI / publish (push) Has been cancelled
2026-09-22 21:36:39 +02:00
lilleman 2f7005590c 22 - the line container is markdown's, so the seam is the two spelling asks
CI / gate (push) Successful in 29s
CI / publish (push) Has been skipped
2026-09-21 22:15:27 +02:00
lilleman 67c3345fd2 21 - the table's row is the node's model, never the node
CI / gate (push) Successful in 29s
CI / publish (push) Successful in 5s
2026-09-21 21:45:11 +02:00
lilleman 7f290d220d 21 - the parse walk reads blocks, so no name means two things
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-21 21:40:43 +02:00
lilleman c2acee7a1d 21 - the ADF tables carry ADF's nouns
CI / gate (push) Successful in 29s
CI / publish (push) Has been skipped
2026-09-21 21:39:59 +02:00
lilleman e964abaa4b 20 - every spelling tried ahead of a directive form takes the try prefix
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 5s
2026-09-21 21:20:47 +02:00
lilleman 3d0926dcaf 19 - adf/ is what both formats read, and a construct rises on its second consumer
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 4s
2026-09-21 10:09:32 +02:00
lilleman 3ee1605ed3 30 - AGENTS.md says each thing once, and the ask protocol has a heading
CI / gate (push) Successful in 29s
CI / publish (push) Successful in 5s
2026-09-20 23:45:08 +02:00
lilleman 0c8fd36ef8 29 - the README states the raw HTML rule once, and the element set is the exception
CI / gate (push) Successful in 36s
CI / publish (push) Successful in 5s
2026-09-20 22:47:43 +02:00
lilleman 39ea801fbe 6 - script and style drop whole, and the set sorts every element three ways
CI / gate (push) Successful in 29s
CI / publish (push) Successful in 4s
2026-09-20 22:08:51 +02:00
lilleman c2bf5c0ea8 6 - the div unwraps, details is an expand, and a comment has nothing to hold it
CI / gate (push) Successful in 29s
CI / publish (push) Successful in 4s
2026-09-20 21:59:03 +02:00
lilleman eed1ac0290 29 - the spec stands: raw HTML routes through the element mapping
CI / gate (push) Successful in 29s
CI / publish (push) Waiting to run
2026-09-20 21:53:07 +02:00
lilleman 77ae596d2e Prose pass: the config is discovered, so -c guards only its absence
CI / gate (push) Successful in 29s
CI / publish (push) Successful in 4s
2026-09-20 18:29:39 +02:00
lilleman f79241f56f Review: name what each ratchet switch guards, IIFEs included
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-20 18:23:46 +02:00
lilleman b969bb36d6 Review: the ratchet counts an IIFE, and no missing config or stray warning passes it
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped
2026-09-20 18:20:10 +02:00
lilleman 6efbac4598 17 - the gate holds every built function to 52 lines
CI / gate (push) Successful in 29s
CI / publish (push) Has been skipped
2026-09-20 18:10:21 +02:00
lilleman 4b2f3b1bb3 AGENTS.md 8: a refusal from our own invariant takes the nearest existing code
CI / gate (push) Successful in 35s
CI / publish (push) Successful in 5s
2026-09-20 18:03:55 +02:00
lilleman 057729f830 Prose pass: the guard's comment goes, and one spelling states the coverage bar
CI / gate (push) Successful in 29s
CI / publish (push) Has been skipped
2026-09-20 17:59:34 +02:00
lilleman 8b5e05f5a9 Review nit: the attempt names the fallback it takes, so no arm is reached by elimination
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped
2026-09-20 17:57:13 +02:00
lilleman ee10b63fe7 Tick 28, moving its text to todo-history.md
CI / gate (push) Successful in 27s
CI / publish (push) Has been skipped
2026-09-20 17:49:54 +02:00
lilleman e34d39fdee 28 - the line's retry takes one fallback per pass, so it cannot spin 2026-09-20 17:49:54 +02:00
lilleman 5c47fb69ad File the emitLine spin as 28, first in the 0.2.0 order
CI / gate (push) Successful in 27s
CI / publish (push) Successful in 5s
2026-09-20 17:23:52 +02:00
lilleman 61faf6f493 The panel's findings become items 19 to 27, and 17 becomes a size ratchet
CI / gate (push) Successful in 27s
CI / publish (push) Successful in 5s
2026-09-20 16:45:01 +02:00
lilleman 665c63af4b The README states its goals and its audience under their own headings
CI / gate (push) Successful in 28s
CI / publish (push) Successful in 5s
2026-09-20 13:15:08 +02:00
lilleman 78b2924fe1 The leg rule covers the host legs and reaches past the function a leg calls
CI / gate (push) Successful in 27s
CI / publish (push) Successful in 5s
2026-09-20 00:12:05 +02:00
lilleman 1fb712c76d A legged function chains with && : the || that captures the status suspends set -e
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped
2026-09-20 00:07:06 +02:00
lilleman be25a7a3f5 Tick 4d, moving its text to todo-history.md
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped
2026-09-19 23:57:06 +02:00
lilleman 6ffafe7536 4d - every gate leg names itself, its image and its seconds 2026-09-19 23:57:06 +02:00
lilleman 00e18d66b5 The memo stops the per-level re-spelling; it does not promise one spelling
CI / gate (push) Successful in 27s
CI / publish (push) Successful in 4s
2026-09-19 13:48:55 +02:00
lilleman cd3bdf0d5d Pin the rebase's magnitude: one overflow list makes a constant accept what the emitter refuses
CI / gate (push) Successful in 27s
CI / publish (push) Has been skipped
2026-09-19 13:43:34 +02:00
lilleman d6756ea5e6 A read below the depth that filled the memo re-spells, so the depth guards still run
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped
2026-09-19 13:37:58 +02:00
lilleman 3f8afcb9d7 Tick 18, moving its text to todo-history.md
CI / gate (push) Successful in 1m26s
CI / publish (push) Has been skipped
2026-09-19 13:18:33 +02:00
lilleman 4ab1ed5132 18 - the parse keeps each node's readable spelling, so the directive ask spells it once 2026-09-19 13:18:33 +02:00
lilleman 7d1463da0d One comment guards the input's shape, the other the field
CI / gate (push) Successful in 37s
CI / publish (push) Successful in 4s
2026-09-19 02:33:23 +02:00
lilleman 4a80e38f19 Pin the deactivation where an image close folds the evidence away
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-19 02:31:03 +02:00
lilleman 20cd21789b Product nits: the decision keeps its asymmetry, the migration row its repair
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-19 02:24:19 +02:00
lilleman 8961cbd20a Literal brackets put nothing inside a mark, so the nested-link check runs ahead of both guards
CI / gate (push) Successful in 37s
CI / publish (push) Has been skipped
2026-09-19 02:21:38 +02:00
lilleman a2928a8789 Review nits: the walk's start needs no clamp, and the invariant needs one copy
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-19 02:15:15 +02:00
lilleman 9d5a8e4137 Pin what the doomed openers keep: the carry and the image literal brackets hold
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-19 02:09:19 +02:00
lilleman 6e9e38776f Review nits: deactivate the openers a nested link dooms, and pin the refusal order
CI / gate (push) Successful in 38s
CI / publish (push) Has been skipped
2026-09-19 02:02:01 +02:00
lilleman 3a22ff388d 16 - no link wraps a link: the outer brackets go literal, the directive form is refused
CI / gate (push) Successful in 37s
CI / publish (push) Has been skipped
2026-09-19 01:48:30 +02:00
lilleman b695c4c88c Review nits: drop the spec's restated carry rule, the test's duplicate paths and todo.md's incident note
CI / gate (push) Successful in 36s
CI / publish (push) Successful in 4s
2026-09-18 23:14:37 +02:00
lilleman 88e5213326 Tick 15, moving its text to todo-history.md and pointing the next session at a fetch first
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-18 23:08:57 +02:00
lilleman 451a617b13 15 - refuse the directive link spelling no href, so the mark has one spelling 2026-09-18 23:08:34 +02:00
lilleman ed099a093d AGENTS.md §11: a sort keys on the name a line introduces, so an import sorts on its first binding 2026-09-18 23:03:13 +02:00
lilleman 5b78e608f0 Tick 14, moving its text to todo-history.md and filing the import-sort ask
CI / gate (push) Successful in 36s
CI / publish (push) Successful in 4s
2026-09-18 21:31:01 +02:00
lilleman f303a815f7 AGENTS.md §11: the shared side is everything outside emit/ and parse/, not the root alone
CI / gate (push) Successful in 36s
CI / publish (push) Successful in 5s
2026-09-18 21:29:00 +02:00
lilleman 42ab63a03b 14 - the CommonMark subset moves under src/markdown/commonmark/
CI / gate (push) Successful in 36s
CI / publish (push) Has been skipped
2026-09-18 20:27:22 +02:00
lilleman 128e55e448 AGENTS.md §15: a session works one chunk and stops, the release being a chain of them
CI / gate (push) Successful in 37s
CI / publish (push) Successful in 5s
2026-09-18 20:21:35 +02:00
lilleman 39039081ee Point the next session at 14, and at a fetch before branching
CI / gate (push) Successful in 36s
CI / publish (push) Successful in 5s
2026-09-18 20:04:19 +02:00
lilleman dfc91a651b Restore the list-item walk's three re-scans in 4c's record, and what became of each 2026-09-18 20:03:56 +02:00
lilleman 0361646e1a 4c - the guard's spread was a class: the code block's held lines and a mark run's segments too 2026-09-18 20:03:56 +02:00
lilleman 047ce3bc42 One release for everything known: 0.2.1 and 0.3.0 fold into 0.2.0 2026-09-18 20:03:56 +02:00
lilleman a774be59f4 Tick 4c, moving its text to todo-history.md and filing its last site as 18 2026-09-18 20:03:56 +02:00
lilleman 7b726ef6ec 4c - the directive scan keeps the spans it read, so the slot parse never scans them again 2026-09-18 20:03:56 +02:00
lilleman 503efa4f82 4c - the label trim, the segment tail, the break run and the guard's spread stop re-walking 2026-09-18 20:03:56 +02:00
lilleman 538e0ce665 AGENTS.md §8 admits a plainly named new code in 0.x, §11 names from the spec
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 33s
2026-09-18 19:38:58 +02:00
lilleman 4147f6623c AGENTS.md §15 asks land as rules; todo.md carries the next-session prompt
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-18 19:31:41 +02:00
lilleman 9d780aa910 Tick 4b, moving its text to todo-history.md
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 39s
2026-09-18 00:52:47 +02:00
lilleman 59b51e8aad 4c gains the document guard's spread as a seventh site
CI / publish (push) Waiting to run
CI / gate (push) Successful in 34s
2026-09-18 00:50:22 +02:00
lilleman c7a7a4333b 4b - review nits: the list's headroom folds into its item loop, emitBlocks inlined, panel-wrapped boundary pinned, 4c note says cubic
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-18 00:43:56 +02:00
lilleman 98ff478fe9 4b - the walk reports its headroom, so a list falling back to the directive form refuses at zero and the list limit stays 500
CI / gate (push) Successful in 34s
CI / publish (push) Has been skipped
2026-09-18 00:34:52 +02:00
lilleman 53ef06f565 4b - the block walk spells a list from one walk, a list and its item counting two levels in every spelling
CI / gate (push) Successful in 33s
CI / publish (push) Has been skipped
2026-09-18 00:13:55 +02:00
lilleman 9a6615c356 todo.md: 15 href-less directive link, 16 link wrapping a link, 17 size guardrail in the gate
CI / publish (push) Successful in 5s
CI / gate (push) Successful in 26s
2026-09-17 23:43:06 +02:00
lilleman 498c73c004 Tick 13 and 13b, moving their text to todo-history.md
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 26s
2026-09-16 23:30:03 +02:00
lilleman 43292555cd 13b - review nits: README code span, AGENTS.md §2 example, comments, block-directive link message
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 23:28:31 +02:00
lilleman b792c946c5 13b - link arm back in emitLink, one isBareLink, directiveMarkPiece, refusal names the spelling, spec subset section follows
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-16 23:15:34 +02:00
lilleman 9fe5d198d1 13b - link takes the directive form where CommonMark cannot spell it, unspellable-link leaves the code list
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 23:09:30 +02:00
lilleman d282470efa Tick 13a, moving its text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 22:37:18 +02:00
lilleman 153808358a 13a - rule's color, style and weight and layoutSection's columnRuleStyle join the tables
CI / gate (push) Successful in 25s
CI / publish (push) Waiting to run
2026-09-16 22:34:12 +02:00
lilleman 17a26b2691 AGENTS.md records that docs on main describe the release being built
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 25s
2026-09-16 22:27:26 +02:00
lilleman e7e78f0d64 Tick 12d and 12, moving their text to todo-history.md
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 29s
2026-09-16 22:21:04 +02:00
lilleman bcbd1b74bc 12d: MIGRATION.md names an empty heading by its localId and one set of empty leaves
CI / gate (push) Successful in 24s
CI / publish (push) Successful in 4s
2026-09-16 22:19:54 +02:00
lilleman 8d07ccd663 12d: MIGRATION.md covers markdown in flight, a second pass and every empty leaf, and the README links absolutely
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-16 22:18:02 +02:00
lilleman 6358a2263c 12d: the README speaks !adf:, MIGRATION.md converts 0.1.0's stored markdown, and 13b records its removed code there
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 22:05:37 +02:00
lilleman d7881d2d3f Tick 12c, moving its text to todo-history.md, and plan 14, the CommonMark subset's directory
CI / gate (push) Successful in 30s
CI / publish (push) Successful in 4s
2026-09-16 20:28:36 +02:00
lilleman 58392ace7c 12c: a test pins the closer to its list item or blockquote edge, and 4c names the list-item walk's re-scan class
CI / publish (push) Waiting to run
CI / gate (push) Successful in 28s
2026-09-16 20:23:00 +02:00
lilleman 582594a6d6 12c: the block walk matches only list items and blockquotes per line and finds a closer's container by name, replacing the depth guard
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 20:08:49 +02:00
lilleman 356b7512c8 12c: the block walk opens no container past the depth the node walk refuses, and the closer messages name the escape
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-16 19:40:30 +02:00
lilleman ba91900b89 12c: one block opener speller beside its reader, the line readers split along the union, and AGENTS.md records the leaf body
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 19:21:06 +02:00
lilleman 4aa9e88238 12c: a leaf given a body faults at its opener 2026-09-16 19:19:15 +02:00
lilleman 5bec3fbbb6 12c: the block directive forms read and spell !adf: openers and closers
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 15:46:52 +02:00
lilleman 2f23c13528 Tick 12b, moving its text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 10:28:08 +02:00
lilleman 9ba2d09ee3 12b: the content slot refuses a carried text node's content too, and the reserved-name message names the live fence
CI / gate (push) Successful in 24s
CI / publish (push) Waiting to run
2026-09-16 10:26:12 +02:00
lilleman 65c09bdbae 12b: the inline opener speller takes its reader's name, and todo.md records 12c's collapses
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-16 10:03:49 +02:00
lilleman 302804cc4e 12b: the inline directive forms read and spell the !adf: prefix
CI / gate (push) Successful in 26s
CI / publish (push) Has been skipped
2026-09-16 09:52:52 +02:00
lilleman fe064efdb9 Tick 12a, moving its text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-16 09:19:51 +02:00
lilleman 16af0b9592 12a: spec/flavour.md and AGENTS.md spell the !adf: grammar
CI / gate (push) Successful in 24s
CI / publish (push) Successful in 4s
2026-09-16 09:16:13 +02:00
lilleman 4ac4a119e4 Tick 4.3 and 4, moving their text to todo-history.md
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-15 22:49:53 +02:00
lilleman ad1f3e996b 4.3: todo.md records the joined-run family and the generalized backtick pass
CI / gate (push) Successful in 25s
CI / publish (push) Successful in 4s
2026-09-15 22:35:48 +02:00
lilleman b6ba440410 4.3: a bare backtick run escapes wherever a later string of its length forms around an escape 2026-09-15 22:28:51 +02:00
lilleman 7d20f3e895 4.3: the ADF generator draws backtick runs beside code spans, and the directive floor's message names what it counts 2026-09-15 22:22:46 +02:00
lilleman 5fe2e6ec42 4.3: the directive floor counts directive-only and carried types in the parsed document, and run parameters name the gate
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:48:06 +02:00
lilleman ef71d55c13 4.3: todo.md records the settled harness, floors and breaks, and 12's and 13b's follow-ups
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:36:55 +02:00
lilleman a3073c69b2 4.3: under the gate seed the markdown property asserts floors on fixpoint and directive-shaped runs 2026-09-15 21:36:55 +02:00
lilleman 8e7d429379 4.3: the shared property module is property-harness.ts, and the canonical arm filters emit refusals 2026-09-15 21:31:41 +02:00
lilleman 755b5cbe01 4.3: the markdown generator splits into inline, leaf-block and block builders 2026-09-15 21:28:32 +02:00
lilleman fe47e6bc7b 4.3: a link opening a paragraph that reads as a link reference definition rides the carry 2026-09-15 21:22:34 +02:00
lilleman aeb54d98d1 4.3: an escaped backtick run escapes whole, and a lone backtick before one escapes too
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped
2026-09-15 21:05:16 +02:00
lilleman 4735933d61 4.3: a paragraph line reading as a link reference definition escapes its opening bracket 2026-09-15 21:05:16 +02:00
lilleman ec5c92d4fd 4.3: the markdown property 2026-09-15 21:05:16 +02:00
lilleman fb10a1dc04 Tick 4.4, moving its text to todo-history.md
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 23s
2026-09-15 20:59:55 +02:00
lilleman 2901a28fcb 4.4: a review comment as a real payload
CI / gate (push) Successful in 22s
CI / publish (push) Successful in 4s
2026-09-15 19:21:53 +02:00
lilleman 047827fd86 4.4: the real payloads 2026-09-15 19:18:49 +02:00
lilleman e23fd9495e Settle 4.4's payloads as editor-written on the test site, and let 4.4 follow 12
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 25s
2026-09-15 17:51:57 +02:00
lilleman 10d63ff8e6 Tick 4.2, moving its text to todo-history.md, and give 13b the autolink fixture
CI / gate (push) Successful in 23s
CI / publish (push) Successful in 3s
2026-09-14 23:45:18 +02:00
lilleman f8149321f2 4.2: the review's runner and weight nits
CI / gate (push) Successful in 23s
CI / publish (push) Successful in 4s
2026-09-14 23:43:49 +02:00
lilleman a581e454ae 4.2: the review's generator, runner and pattern fixes
CI / gate (push) Successful in 23s
CI / publish (push) Has been skipped
2026-09-14 23:25:13 +02:00
lilleman 342c2027d8 4.2: a link target's would-be directive escapes inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman fade9f51ea 4.2: generated keys avoid V8's JSON.parse key fault 2026-09-14 23:25:13 +02:00
lilleman b6930b45a4 4.2: an autolink holding a backtick gives way inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman e166e969cb 4.2: the ADF property 2026-09-14 23:25:13 +02:00
lilleman 507f68aa13 4.2: a link target's backtick escapes where it would open a code span inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman a2672bdc5b 4.2: a list whose item holds a whitespace-only line takes the directive form 2026-09-14 23:25:13 +02:00
lilleman 9303235434 4.2: a link's brackets balance inside a directive's content 2026-09-14 23:25:13 +02:00
lilleman eafd863a32 4.2: an empty link destination beside a title spells <> 2026-09-14 23:25:13 +02:00
lilleman 1125715371 Record the autolink backtick answer and the accepted V8 JSON.parse fault in 4.2
CI / publish (push) Successful in 4s
CI / gate (push) Successful in 19s
2026-09-14 22:34:05 +02:00
lilleman 31156f4914 Settle 4.2's generator mix and how the breaks it finds get fixed
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 18s
2026-09-14 20:50:17 +02:00
lilleman 0185879943 Tick 4.1, moving its text to todo-history.md, and record where 4.4's payloads land
CI / publish (push) Successful in 3s
CI / gate (push) Successful in 18s
2026-09-14 20:23:41 +02:00
301 changed files with 12532 additions and 3572 deletions
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"categories": { "correctness": "off" },
"ignorePatterns": ["src/**/*.test.ts", "src/conformance/property-harness.ts"],
"rules": {
"eslint/max-lines-per-function": ["error", { "IIFEs": true, "max": 52, "skipBlankLines": false, "skipComments": false }]
}
}
+153 -304
View File
@@ -1,355 +1,204 @@
# Working in this repo # Working in this repo
Decisions a reader would otherwise relitigate, and the rules for every collaborator, human or The rules for every collaborator, human or agent, and an index of the decisions a reader would
agent. Using the library: `README.md`. What is still to build: `todo.md`. otherwise relitigate. Using the library: `README.md`. What is still to build: `todo.md`.
## 1. Three formats, ADF is the hub ## Decisions
ADF, one markdown flavour, one HTML dialect. Six directions exposed, but markdown↔HTML compose In `docs/decisions.md`:
through ADF: four conversions exist to keep correct — never write a fifth. No fourth format, ever;
each one doubles the directions.
## 2. The round-trip is the product - Plain markdown is a flavour of the grammar
- The round-trip is the product
- Markdown in is a canonical fixpoint
- Equality is deep
- `!adf:textBreak{}` parts text CommonMark would join
- An empty key spells `empty`
- `-0` is spelled `-0`
- Empty markdown is a document of no blocks
- Unknown nodes ride the carry
- The carry fence names the node type
- A code block is a fence per text node
- Foreign HTML sorts three ways
- Names stay text
- Directives under `!adf:`
- CommonMark is a subset
- Tables
- Links
- Ids stay site-local
- Plain task ids come from position
- A callout title keeps its link targets
- The plain flavour's spellings
- The HTML dialect
- No runtime dependencies
- Standards ship as data
- fast-check
- Any ES2022 engine
- ESM only
- One built entrypoint
- Public on npm
- The formats are API
- The code list
- Which code a cause takes
- `message` and `path`
- Publish on a version bump
- Docs describe the release being built
- No schema validation
- The gate runs on Deno and Bun
- The gate installs the tarball
- Firefox reads the build
- The coverage floors
- The size ratchet
- The project ships under the comprehension floor until items 59, 60, 61 and 62 land
- Properties on a fixed seed
- The CommonMark suite checks three ways
- The flavour spec is read as a source
- The node tables answer to Atlassian's schema
- Nothing recurses unbounded
- Nothing spreads an unbounded array
- A retry loop checks its own termination
- Readers scan by index
- The spelling memo
- Cost fixes are measured, never timed
- Only the hard break holds a raw newline
- Emphasis follows CommonMark's matching
- Readable spellings take the `try` prefix
- The attribute vocabulary is ADF's
- The source parts by ADF and format
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything ## 1. Nothing about any consumer
less silently destroys content an editor could not represent, in a document it did not author.
When losslessness and readability conflict, losslessness wins.
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 —
where there is a way back. CommonMark spells some things the flavour has no escape for — a link
destination or title holding a backslash or newline, a paragraph opening with a code span whose
backticks read back as a fence — so a parse succeeding does not imply a spellable document;
`corpus/commonmark-spec/exceptions.json` names those.
"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks
and no attributes merged, JSON number semantics, an empty attrs object, marks array or content
array the absent key — the only domain markdown can restore.
Round-trip equality is a property tested over a corpus, not a claim made in prose.
## 3. Unknown input policy
- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats and
restores to a deep-equal node. The round-trip holds for documents newer than the library. So
does a known node no section spells where it stands: a markdown serializer spells a node by type
without checking its position, and refusing loses a document ADF itself keeps in an
`unsupportedBlock`. Where a container's own spelling cannot hold the child it has — a
`bulletList` outside `listItem`, a `codeBlock` outside text — the error result names that
instead.
- Unmappable foreign HTML element: error result naming the element — never a silent drop.
- Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce
mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job.
## 4. The flavour
- Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks,
`:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal.
- 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 — plus one image gap.
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
site — accepted.
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
cannot express, text always escaped. No stylesheet ships.
## 5. Dependencies
`dependencies` is empty. A runtime dependency enters only through a decision entry here stating
why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the
CommonMark and HTML parsers are written in this repo. 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. The CommonMark spec suite is the same shape of
data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev
dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently
point the vendored exception list's example numbers at a renumbered suite. A spec bump is a
deliberate re-pin, exceptions re-derived by hand beside it. Atlassian's ADF JSON Schemas ship
vendored the same way, at `spec/adf-schema/`, rather than as the `@atlaskit/adf-schema` dev
dependency — CommonJS-only, some fifty packages with React among them, and a release most days for
Renovate to automerge — re-pinned by hand when a payload or a report shows the need.
`devDependencies`: few, each earning its keep; they never reach a consumer. `fast-check` earns its
place shrinking a failing generated document to the nodes that break it.
## 6. The package contract
- 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. `tsconfig.build.json` is
that gate, typechecking and emitting the shipped files alone, so `node:fs`, `process` and an
ES2024 method are compile errors here rather than a consumer's crash there. The standard is the line, never an
engine list: one implementing it in part — Hermes is the live doubt, on §10's property escapes
and on lookbehind — is out of scope rather than a bug. Node's test runner, the corpus reads and
the build are the repo's own,
never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` —
never the higher one those repo-only tools want.
- ESM only — no CommonJS build, no dual-package hazard.
- One entrypoint: 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 an npm consumer.
- Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo
goes public, LICENSE in place, before the first publish.
- Exact versions: `save-exact=true` in `.npmrc`.
## 7. Nothing about any consumer
No Jira client, no HTTP, no REST shapes, no issue keys, no actual consumer named anywhere. Design No Jira client, no HTTP, no REST shapes, no issue keys, no actual consumer named anywhere. Design
against the README's personas. against the README's personas.
## 8. Semver: the formats are API ## 2. Release automation
The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing - The bump commit renames `CHANGELOG.md`'s `## Unreleased` to the version.
differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR. - Exact versions: `save-exact=true` in `.npmrc`.
Pre-1.0, normal 0.x rules.
The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the
code from a closed list a consumer may switch exhaustively, the message free text, the path the
node's place from the document root, the position where a parse read the refusal in its input.
A message names the violation, not the rule alone — a rule by itself states a truth the reader
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it
names the escape that unclaims the form claimed: `\:::` for a directive line, `\|` for every pipe
row, `\:` for an inline directive.
Adding, removing or renaming a code is breaking, so a milestone meeting a new failure cause
reuses a code where one fits; the list is complete at `0.1.0`. A code names the
cause; where one cause recurs across node types, across one mark's attributes or across
directions, one code covers them all and
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
direction hits it, `unspellable-link` the destination and the title alike. Where two codes stay
apart, the line between them is what they name: `unspellable-character` is a character CommonMark
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
a malformed `:::table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` —
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
for. A cause the carry answers gets no code: a mark no
spelling writes rides the carry with its node. A directive whose name reads back to no node is
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
reserved name is a known name, so never that code, and the two the flavour reserves part on form:
a form the grammar does not have is a claim code — `::adf`, whose carry is the fence — and a
well-formed form in the wrong place is `unsupported-node-shape`, `::listBreak` parting anything
but two adjacent lists of one type. What
the grammar itself refuses stays a claim code, key order among it; a well-formed directive the
node tables refuse — an attribute a node does not hold or spells elsewhere, a value outside its
kind or its canonical spelling, an argument or a body its content model does not take — is
`unsupported-node-shape`, the emitter's code for the same mismatch read the other way — one code
across both directions for good, since the call site knows which direction it called and parting
them after `0.1.0` is MAJOR. `unmappable-html` names the version rather than the element: this one
converts no raw HTML, so at `0.3.0` the mapped elements stop erroring and the code stays for what
no ADF node carries. A refusal found before its path is known — the block walk's, a directive
reader's — is a `ConvertFault`, the code and message alone; the node walk attaches the path as it
descends, so a document reports its first error in document order. `not-an-adf-document` carries
the document's own path throughout: eight of the guard's nine branches read the document's own
shape, and threading a path to the ninth — a malformed node anywhere in the tree — wants the
manual stack §11's no-recursion rule forces, whose empty half no input reaches. The message names
the violation instead. Depth is not one of the nine: `adfDocumentFault` returns the code with the
message, so an attribute value past 500 levels is `unsupported-nesting-depth` from the emitter as
it already is from the parser, both directions refusing the same value. A node's attribute is
counted from the value itself, never from the `attrs` object holding it; a mark's is counted three
levels in, because the block directive spells the whole mark set as one JSON attribute and the
parser reads the value at the bottom of array, mark and `attrs`. `isAdfDocument` is true for a depth fault:
a deep document is a document, as the 2000-level blocks and the 600-deep marks the guard already
waves through are, and depth is the walks' answer rather than the shape's. A non-finite number
stays parted where depth is joined: the parse says `unsupported-node-shape` because the markdown is
at fault, the emit `not-an-adf-document` because the input is, and unlike depth nothing round-trips
inconsistently between them.
`position` is the parse side's alone: an emitter reads no source, so an emit error carries `path`
and nothing more. It is `{ line, offset }` at the start of the line the block holding the refusal
begins on — the offset indexing the string the caller passed, the line counted from 1 — minted by
the block walk and attached as results return, so the innermost block wins, the emitter's own
refusals the parser re-enters for the CommonMark spelling included.
A parse names a position for every refusal it returns, so the type says so rather than the prose:
`Result<T, E extends ConvertError = ConvertError>`, and a direction reading a source returns
`Result<T, ParseError>` — `ConvertError` with `position` required. An optional field a direction
always fills is a branch a consumer cannot take, and the `!` §11 bans is how they take it anyway.
`htmlToAdf` inherits this at `0.3.0`; the composed `markdownToHtml` and `htmlToMarkdown` keep the
wide `Result<T>`, since half their refusals come from an emit stage that read no source.
## 9. Release automation
- `package.json` version on `main` is the source of truth. CI on `main`: tests green and version
differs from npm → publish and tag `vX.Y.Z`. No bump, no deploy; the bump is each shipping PR's
deliberate semver judgment. `publish.sh` is that job, and `private: true` stops it before it
reads the token, so the pipeline is live and silent until the maintainer's first bump drops the
field.
- The publish and the tag each observe their own end state — the version on npm, the tag on the
remote — and neither gates the other, so a run that dies between them converges on the next push
to `main` rather than leaving npm ahead of the tags. An unanswered registry reads the same as an
unpublished version, which npm's own duplicate rejection is what catches. The job rebuilds rather
than taking the gate's `dist`: the lockfile is committed, the image is patch-pinned and `tsc` is
deterministic, so the two builds agree, and promoting an artifact would make the release path
depend on a store that the gate would then have to keep.
- Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI. - Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI.
- Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as - Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as
specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base
floating because Bun publishes nothing narrower. Actions pin semver tags. floating because Bun publishes nothing narrower. Actions pin semver tags.
## 10. Tests first, in Docker ## 3. Tests first, in Docker
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code. Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent, Node, tsc and npm never run on the host — only via the pinned images (§2). Tests are independent,
coverage does not decline, containers are torn down after a run. containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:`
shims all carry.
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike, Every leg announces its name and, where a container is in play, the image, before it runs and its
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier, elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
so it holds the module graph to the fully-spelled form a browser can load; Bun runs a hang. A leg added later owes the same marker, and a function a leg reaches chains its statements
JavaScriptCore, the one engine of the three that is not V8, where the Unicode property escapes with `&&`, because the `||` that captures the leg's status suspends `set -e` for everything it
emphasis matching leans on can disagree. Both refuse a run matching no test, so Node's is the only calls. A leg whose output is both streamed and grepped keeps the copy in a `mktemp`
vacuous-green guard, and a test may reach only for what all three `node:` shims carry — the price file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL
of proving those engines over the corpus rather than over a smoke import. holes through each other's lines.
The gate then packs the build and installs the tarball under `package-tests/`, so `files`, `PROPERTY_RUNS=<runs>` raises the property runs and randomizes the seed for local digging. The
`exports` and `types` are proved on the artifact that ships rather than on the source tree a generators and run parameters properties share live in `src/conformance/property-harness.ts`,
self-reference would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside outside the build and coverage.
`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers
`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's
resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven.
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js` Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares
over HTTP and converts the round-trip, normalization and error fixtures — the `commonmark-spec` the nodes named before its first em dash, with the attributes following `Attributes: ` — a
sort is the Node suite's to check — which is §6's browser half and the only SpiderMonkey parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet.
there is — the gate's other three engines are two V8s and a JavaScriptCore that is not Safari's.
A WebDriver session is what carries a verdict back out, the driver and the page's server sharing
one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such
channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser.
The leg re-checks the conversions and nothing else — each fixture's emitted markdown, its parsed
document, its error code — leaving the corpus's pairing, uniqueness, source positions and
byte-level equality to the Node suite that owns them.
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and ## 4. Code rules
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
compared against `undefined` — have a half no valid document reaches.
The corpus, all checked in: hand-built fixtures per node and combination; real sanitized ADF from - Two-space indent, English everywhere. Alphabetical order wherever order carries no meaning,
live Atlassian APIs; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`. keyed on the name a line introduces: an import sorts on its first binding, type imports ahead of
value imports, so moving or renaming a module reorders nothing.
Beside the corpus, properties run over documents generated from the node tables, on a fixed seed in
the gate; `PROPERTY_RUNS=<runs>` raises the runs and randomizes the seed for local digging, and a
counterexample found becomes a round-trip fixture.
`spec/flavour.md` is read as a source too, so the node tables cannot drift from the prose they
copy: each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes
named before its first em dash, with the attributes following `Attributes: ` — a parenthesized
value set reading `string` — and must equal the tables in `adf/`. Keep prose in those sections out
of a bullet; fenced examples are skipped. It guards the attributes alone: nodes that differ in
content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so
both answer to the round-trip corpus and to nothing else where a node has no fixture.
The tables answer to Atlassian's schema too (§5): for every node and mark they spell, the attribute
names and kinds equal what `full.json` and `stage-0.json` hold between them. Value sets stay
documentation, since any value round-trips. What the schema holds and the tables do not spell is
pinned by name — an attribute as a gap, a type as carried — so a re-pin adding either goes red until
someone spells it or pins it.
## 11. Code rules
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
carries no meaning.
- Failures are values: everything returns
`Result<T>` — `{ ok: true; value } | { ok: false; error: ConvertError }` — nothing throws.
`try/catch` only wrapped tightly around a call that genuinely throws, converted to a result on
the spot. A reader with no path to name returns `Read<T>` instead, the same two arms over a
`ConvertFault`, and `faulted` attaches the path where the walk knows it.
- Only the hard break's inline segment holds a raw newline — every other spelling escapes one or
refuses it — which is how the whitespace carry finds a line edge.
- Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the
only ones in play, and a pair that matching hands to another delimiter rides the carry instead.
`matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and
opener search stay whole: broken into named steps they drift from the algorithm being faithful is
the whole point of.
- A readable spelling tried ahead of a general one — a CommonMark block, the image, the pipe
table, a pipe cell — gives way with `undefined` for every shape it cannot spell, and fails only
where the general form fails on the same node. Refusing there refuses a document the general
form spells, so a refusal the general form does not share belongs in the general form or
nowhere — save the nested list a tight spelling would swallow, whose refusal the
tight-versus-blank answer owns (`todo.md` 2b).
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
`Result` rather than the stack overflow that waits near 2000.
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
hoists the scan that does not vary with the character. The pipeline persona feeds documents
nobody typed, and a megabyte through a quadratic walk is a minute rather than a millisecond.
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the - No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
unrepresentable. unrepresentable.
- `src/adf/` holds ADF's own knowledge and imports no format. Each format directory (`markdown/`, - Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped
`html/`) parts into `emit/` (ADF→format) and `parse/` (format→ADF), its root holding what both tightly around a call that genuinely throws, converted to a result on the spot. A reader with no
directions read. A construct's reader lives in that root beside the regex the emitter escapes path to name returns `Read<T>`, and the walk attaches the path where it knows it.
against, so the two cannot drift; a reader with no emit counterpart goes in `parse/`, unless it is
part of a construct the root already holds — a grammar stays in one file rather than splitting
across the seam. A rule both
directions must answer alike — whether a list marker interrupts a paragraph — is one function
there too, never a copy per direction, however conservative the copy would be. Where the rule is
the emitter's own choice, input consults it rather than restating it: the parser asks
`commonMarkSpelling` which form the emitter picks, so no fixture the emitter writes can be
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a
number is the markdown flavour's choice, not ADF's.
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a
file does not repeat its directory in its name — `adf/document.ts`, never
`adf/adf-document.ts`.
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality — - Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
a second consumer, or it goes. a second consumer, or it goes.
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a file
does not repeat its directory in its name — `adf/document.ts`, never `adf/adf-document.ts`. A name
is the noun `spec/flavour.md` or ADF's schema uses for the thing.
## 12. Prose to a minimum ## 5. Prose to a minimum
Applies everywhere: comments, every markdown file in this repo (this one included), PR text. Applies everywhere: comments, every markdown file in this repo (this one included), PR text.
- Default is no comment. One earns its single line only by naming an invariant, footgun or - Default is no comment. One earns its single line only by naming an invariant, footgun or
external constraint the code cannot show — never restatement, history, absence or arrangement. external constraint the code cannot show — never restatement, history, absence or arrangement.
A second line belongs in the commit message or a decision entry here. A second line belongs in the commit message or a `docs/decisions.md` entry.
- Every prose comment in a diff is a review question; the default answer is delete.
- A doc paragraph says what the repo cannot say for itself, or it goes. The fix for a redundant - A doc paragraph says what the repo cannot say for itself, or it goes. The fix for a redundant
one is deletion, not trimming. A false claim in any doc is a bug, fixed where found. one is deletion, not trimming. A false claim in any doc is a bug, fixed where found.
- Published text — npm README, error messages, API docs — never references internal systems, - Published text — npm README, error messages, API docs — never references internal systems,
tickets or repos. tickets or repos.
## 13. Commits and PRs ## 6. Commits and PRs
One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever. One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever.
## 14. Non-goals ## 7. The working loop
No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema A session works one chunk, starting from the first item in `todo.md` not waiting on an unmet "Lands
validation or exported validator — a refusal that keeps the round-trip is not schema validation, after", and stops when that chunk merges, whatever it was asked to finish: a release is a chain of
so the one a spelled node carrying the same mark type twice earns stays, and input nesting a sessions, so an instruction to work until a release is done names the chain, not the session. An
spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS (§4), no open PR is a chunk already in flight, and finishing it is the session.
streaming APIs, no performance budget past §11's scanning rule — nothing here is tuned, and no Per chunk:
figure is promised. A CLI is a later goal (`todo.md`), not a non-goal.
## 15. The working loop 1. Fresh worktree off updated `origin/main`; implement tests-first (§3).
One unchecked `todo.md` item per session, in the smallest PR-able chunk — split a big milestone
into sub-items in `todo.md` before starting it. A chunk running a little over or under that is not
worth deliberating; what matters is that nothing is left undone in the end. Per chunk:
1. Fresh worktree off updated `origin/main`; implement tests-first (§10).
2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest 2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest
gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a
result exists for the commit under review, or when the diff since that result cannot affect result exists for the commit under review, or when the diff since that result cannot affect
it (docs-only) — re-run only what its own findings or fixes invalidate. it (docs-only) — re-run only what its own findings or fixes invalidate.
3. Merge the PR (standing authorization, this repo only, granted through the `0.2.0` release — 3. Merge the PR (standing authorization, this repo only, granted by the maintainer through the
the maintainer, 2026-09-13), check the box in `todo.md` and move the item's text to `0.2.0` release), report, stop.
`todo-history.md`, leaving its title behind, report, stop. The next chunk gets a fresh session.
Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked, Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a
and the answer lands as a decision in this file. The confidence bar is very high — asking too bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the
often is the accepted cost, guessing wrong is not. maintainer's) and the `NPM_TOKEN` secret.
Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on ### Ask, don't guess
`main` publishes, §9 — every release is the maintainer's) and the `NPM_TOKEN` secret.
A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each Any choice where what the maintainer would pick is not near-certain gets asked. The confidence bar
iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything is very high — asking too often is the accepted cost, guessing wrong is not.
remembered from earlier iterations. The loop session is a thin driver: each chunk's work runs in
a fresh-context subagent holding this file as its charter, and the driver only relays maintainer An ask is a gap in `docs/decisions.md`, and its answer is the entry that closes it, landing there —
questions, runs the review flow, merges, and cleans up. The loop stops when only never the instance alone; an answer that is a goal lands in the README, one that is a working rule
maintainer-reserved acts remain. here. Before asking, name the class the question belongs to and the entries of that class; where
one already decides it, apply it without asking, and where it reads two ways on this input, that
reading is the ask. Never ask "A or B?": state the gap, the earlier entries of its class, the
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
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
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
The verdict lands in `docs/decisions.md`.
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
are the people who read and write that format (README `## Audience`). A panel of the developer
personas settles a question about what an app relies on.
### Stated numbers
A stated number — 500 levels, the branch floor — is kept; a chunk that cannot keep it asks, naming
the number it can reach. A number the code needs and no entry states is a gap.
### The continuous loop
A `/loop` session counts as a chain of sessions, each iteration starting by re-reading `AGENTS.md`
and `todo.md` and trusting them over anything remembered from earlier iterations. The loop session
is a thin driver: each chunk's work runs in a fresh-context subagent holding this file as its
charter, and the driver only relays maintainer questions, runs the review flow, merges, and cleans
up. The loop stops when only maintainer-reserved acts remain.
## 8. Scoring run
The comprehension panel's fill-ins:
- Language: TypeScript.
- Kind: a pure-function document converter with hand-written parsers and emitters.
- Domain: Atlassian Document Format and CommonMark parsing.
- Domain docs: the CommonMark spec, ADF's JSON schema and `spec/flavour.md`.
- 3am question: a viewer/editor app reports that a document it saved comes back with two text
nodes merged and a mark gone after `markdownToAdf(adfToMarkdown(doc))`.
+39
View File
@@ -0,0 +1,39 @@
# Changelog
## Unreleased
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
`!adf:` is claimed. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
spells a document holding no `content` key.
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as `JSON.parse` builds it: two adjacent text
nodes CommonMark would read back as one are parted by `!adf:textBreak{}`, an empty `attrs`,
`content` or `marks` is spelled `{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is
spelled `-0`, and a `codeBlock` of several text nodes is a fence per node. A `codeBlock` holding
other than plain text nodes rides the block carry, where it was refused.
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
`unsupported-node-shape`, and an empty node's leaf and closed spellings swap which one parses;
`MIGRATION.md` lists each.
- **Breaking:** a link whose text holds another link keeps the inner link and leaves the outer
brackets literal text, where `0.1.0` split the outer link around it; see `MIGRATION.md`.
- Add `adfToPlainMarkdown` and `plainMarkdownToAdf`, a lossy pair converting ADF to and from
markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and
pipe tables.
- Spell `rule`'s `color`, `style` and `weight`, `layoutSection`'s `columnRuleStyle` and a link's
`collection`, `id` and `occurrenceKey` directly where they rode the opaque carry.
- Fix an image inside another image's description: it flattens into the alt text, where it was
refused.
## 0.1.0
- First release: lossless conversion between ADF and an extended markdown flavour —
`adfToMarkdown`, `markdownToAdf` and `isAdfDocument`. Nothing throws, and every error carries a `code` from a
closed list.
+75
View File
@@ -0,0 +1,75 @@
# Migrating
## From `0.1.0` to `0.2.0`
Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error,
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
`0.1.0` built that shape from empty markdown, meaning the empty document.
### Convert markdown
Read it with `0.1.0` and write it with `0.2.0`, installed side by side:
```sh
npm install @larvit/adf-codec@0.2.0 adf-codec-0.1@npm:@larvit/adf-codec@0.1.0
```
```ts
import { adfToMarkdown } from '@larvit/adf-codec'
import { markdownToAdf as markdownToAdf010 } from 'adf-codec-0.1'
function migrateMarkdown(stored: string) {
const parsed = markdownToAdf010(stored)
// 0.1.0 dropped an empty content array, so a document with no content key meant an empty one.
return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed
}
```
- Convert each document once: a second pass can return ok while turning the directives into text.
Stop `0.1.0` writing first, and record which documents are converted.
- A refusal carrying `position` is `0.1.0`'s parse, which refused that markdown before too. One
without is `0.2.0`'s emit: store the document `markdownToAdf010` read as ADF, with
`content: parsed.value.content ?? []` as the recipe gives it, rather than keeping the unconverted
markdown.
### Spellings
| `0.1.0` | `0.2.0` |
| --- | --- |
| `:::panel info` … `:::`, the fence longer per nesting level | `!adf:panel info` … `!adf:/panel` at any depth |
| `::media {id=a type=file}` | `!adf:media {id=a type=file}` |
| `::taskItem TODO {localId=i}`: an empty `caption`, `decisionItem`, `paragraph` or `taskItem`, or an empty `heading` carrying `localId` | `!adf:taskItem TODO {localId=i}` then `!adf:/taskItem` |
| `:mention[@Mikael]{id=5b10a2}` | `!adf:mention[@Mikael]{id=5b10a2}` |
| the `adf` code fence and `:adf{json="…"}` | the `adf:<type>` code fence, its JSON without `type`, and `!adf:carry{json="…"}` |
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` |
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are claimed instead.
### Readings
Markdown the spelling table leaves alone, which `0.2.0` reads as a different document.
| Input | `0.1.0` | `0.2.0` |
| --- | --- | --- |
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
| markdown holding no block (`markdownToAdf("")`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | the block carry, refusing a body that is not one node's canonical JSON; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block |
### Error codes
`unspellable-link` leaves `ConvertErrorCode`: a `switch` naming it stops compiling, and the link
it named converts.
| Input | `0.1.0` | `0.2.0` |
| --- | --- | --- |
| a link whose `href` or `title` no CommonMark escape spells, on emit | `unspellable-link` | spells `!adf:link[text]{attrs}` |
| a `codeBlock` holding other than plain text nodes, on emit | `unsupported-node-shape` | rides the block carry |
| a leaf node given a body (`media`, `listBreak`) | `unsupported-node-shape` | `malformed-directive` |
| a node with a block body written as a leaf (`panel`) | `unsupported-node-shape` | `malformed-directive` |
| an empty node the `::taskItem` spelling row names, written as a leaf | parses | `malformed-directive` |
| an empty node the `::taskItem` spelling row names, written with a closer | `unsupported-node-shape` | parses |
+158 -59
View File
@@ -4,9 +4,13 @@ Lossless conversion between **Atlassian Document Format** (ADF), an extended mar
an HTML dialect. an HTML dialect.
**Status: published — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML at **Status: published — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML at
`0.3.0`.** `0.2.0`.**
Plan: `todo.md`. Decisions: `AGENTS.md`. The flavour's grammar: Plan:
[`spec/flavour.md`](spec/flavour.md). [`todo.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/todo.md). Decisions:
[`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md). Changes:
[`CHANGELOG.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/CHANGELOG.md). The lossless flavour's grammar:
[`spec/flavour.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/spec/flavour.md).
Upgrading from `0.1.0`: [convert your markdown first](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/MIGRATION.md).
## What it is for ## What it is for
@@ -18,6 +22,40 @@ one-directional and lossy. A consumer that shows a document and lets someone edi
directions lossless — otherwise saving destroys the panels, mentions and attachments it could not directions lossless — otherwise saving destroys the panels, mentions and attachments it could not
represent. represent.
## Goals
The most useful ADF conversion library available, judged by these goals, in priority order:
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.**
9. **Easy to find, and clear at a glance what it does.**
## Audience
Application developers embedding the library, in four personas. All four rely on the guarantees
below and on an error's `code` being a closed list; none may rely on an error message's wording,
which is free text.
- **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the
round-trip holding for whatever the site's editor wrote, unknown node types included, and on a
refusal arriving before the save rather than after.
- **Bot posting content** — turns generated markdown into ADF. Relies on plain CommonMark being
valid input, so nothing upstream has to learn a flavour.
- **Export/indexing tool** — converts ADF to markdown or HTML in bulk. Relies on readable output
and on every refusal being deterministic, so a document that fails fails the same way next run.
- **LLM/agent pipeline** — hands documents to a model as markdown and writes the edits back.
Relies on the round-trip and on markdown a reader half-knowing the lossless flavour can still edit.
Behind those apps, the people who read and write the markdown, and later the HTML: product
managers, engineers and support agents working in Atlassian products through a plugin or another
UI. They know markdown and not ADF, and rely on every spelling saying what it means to them.
## The shape ## The shape
```sh ```sh
@@ -27,7 +65,7 @@ npm install @larvit/adf-codec
```ts ```ts
import { markdownToAdf } from '@larvit/adf-codec' import { markdownToAdf } from '@larvit/adf-codec'
const result = markdownToAdf('# Release notes\n\n:::panel info\nShipped on Tuesday.\n:::\n') const result = markdownToAdf('# Release notes\n\n!adf:panel info\nShipped on Tuesday.\n!adf:/panel\n')
if (result.ok) { if (result.ok) {
send(result.value) send(result.value)
} else { } else {
@@ -36,25 +74,84 @@ if (result.ok) {
} }
``` ```
Pure functions, no I/O, no configuration. ADF is the hub: markdown↔HTML compose through it. Serves Goals 1, 2 and 7. 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.
```ts ```ts
adfToMarkdown(doc: AdfDocument): Result<string> adfToMarkdown(doc: AdfDocument): Result<string>
markdownToAdf(markdown: string): Result<AdfDocument, ParseError> markdownToAdf(markdown: string): Result<AdfDocument, ParseError>
isAdfDocument(v: unknown): v is AdfDocument isAdfDocument(v: unknown): v is AdfDocument
adfToHtml(doc: AdfDocument): Result<string> // 0.3.0 adfToPlainMarkdown(doc: AdfDocument): Result<string>
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.3.0 plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError>
markdownToHtml(markdown: string): Result<string> // 0.3.0, via ADF
htmlToMarkdown(html: string): Result<string> // 0.3.0, via ADF adfToHtml(doc: AdfDocument): Result<string> // 0.2.0
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.2.0
markdownToHtml(markdown: string): Result<string> // 0.2.0
htmlToMarkdown(html: string): Result<string> // 0.2.0
``` ```
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws. `Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
## Plain markdown
Serves Goal 6. 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
`not-an-adf-document`, `unsupported-document-version` and `unsupported-nesting-depth`, and writes
no directive.
`plainMarkdownToAdf` reads what `markdownToAdf` reads and refuses what it refuses, and reads the
conventions below as nodes, taking other tools' spellings too; a backslash keeps a marker as text:
`\==x==`, `> \[!NOTE]`, `- \[x]`. Markdown `adfToPlainMarkdown` wrote reads back and writes again
byte for byte; the document it came from does not come back.
To edit a document and save it back, use `adfToMarkdown` and `markdownToAdf`: saving what this pair
read replaces mentions, attachments and macros with text.
| ADF | Written | Read back |
| --- | --- | --- |
| `panel` | a GitHub alert, `> [!WARNING]`: info `NOTE`, note `IMPORTANT`, tip and success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE` | `NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error, and Obsidian's: hint tip; success, check, done success; attention warning; danger, failure, fail, missing, bug, error error; any other word info — in any case; the rest of the marker's line is the first paragraph |
| `expand`, `nestedExpand` | Obsidian's folded callout, `> [!NOTE]- Title` | `-` or `+` after any word, the rest of the marker's line the title; a link reads `text (target)`, or its text alone where the text is the target with or without `mailto:`; an expand inside an expand is a `nestedExpand` |
| `taskList` | `- [x] Done`, `- [ ] Todo` | a bullet list whose every item is so marked, `[X]` too |
| `backgroundColor` | `==text==` | `==text==` on one line, the text touching both delimiters, bounded outside by whitespace, punctuation or a line edge, or touching a Han, Hangul, Hiragana, Katakana, Thai, Lao, Khmer or Myanmar character on either side, in the editor's default highlight `#f8e6a0` |
| `table` | a pipe table: the first row its header, a cell's blocks on one line, a span kept under its header by empty cells | — |
| `decisionList` | a bullet list | — |
| `mention`, `status`, `emoji`, `date` | their text: `@` kept, a mention with none `@` and its id, an emoji its `shortName` without, a date `2026-09-13` in UTC | — |
| `inlineCard`, `blockCard`, `embedCard` | a link to the card's URL, else its name | — |
| external `media` | `![alt](url)` in a block, `[alt](url)` inline | — |
| stored `media`, `mediaInline`, `extension`, `inlineExtension` | their `alt` or `text` | — |
| `layoutSection`, `bodiedExtension`, `bodiedSyncBlock`, `multiBodiedExtension`, `extensionFrame`, `caption`, a node this version does not know | its blocks or its text | — |
| `placeholder` | nothing | — |
- Content the document only references, with no text of its own to keep, leaves an italic note
naming it where it stood: `_(image not included)_` for stored media with no `alt`,
`_(link card not included)_` for a card with neither URL nor name, an extension with no `text`
its key, `_(jira-issues-table not included)_`, or `_(extension not included)_` without one, and
`_(synced block not included)_` for a `syncBlock`.
- `backgroundColor`, `code`, `em`, `link`, `strike` and `strong` stay; every other mark drops,
keeping its text, and so does a mark the flavour cannot spell where it stands.
- 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.
- 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
bullet list keeping its states as text: `- \[x] Done`.
- Text that would read as a marker takes a backslash: `==` wherever it could open or close a
highlight, `[!…]` opening a quote, and `[x]` or `[ ]` opening any list item, since GitHub reads
that marker per item.
- A node read back carries no `localId` except a `taskList`, `taskItem` or `blockTaskItem`, which
Atlassian's schema requires one on: each gets a UUID v4 hashed from the whole markdown and its
position, the same on every read. Join markdown bound for one document and read it once: the same
markdown read twice into one document repeats its ids.
## The errors ## The errors
An ADF node type this version does not know is not an error: it is carried opaquely and restores Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair
unchanged (AGENTS.md §3). carries it opaquely and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
`ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`, `ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`,
stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text
@@ -74,17 +171,17 @@ UTF-16 code unit, a JavaScript string index rather than a codepoint or a byte of
or before the refusal — currently the start of the line the enclosing block begins on; a later or before the refusal — currently the start of the line the enclosing block begins on; a later
minor may narrow that, never widen it. minor may narrow that, never widen it.
Parsing — `markdownToAdf`, and `htmlToAdf` at `0.3.0`: Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`:
| Code | Fires when | What you can do | | Code | Fires when | What you can do |
| --- | --- | --- | | --- | --- | --- |
| `malformed-directive` | a `:::` block or `:name[…]` inline directive the grammar cannot read — an unclosed fence or `[content]`, `{attrs}` out of order or duplicated, invalid JSON in an `adf` carry | write the spelling the message names, or escape the line — `\:::` for a block, `\:` for an inline one — to keep it literal text | | `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` |
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way | | `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the colon; the spelling itself is well formed, so a later minor may give the name meaning | | `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
| `unmappable-html` | the markdown holds a raw HTML tag, comment or processing instruction | remove it or write it in the flavour — ADF holds no raw-HTML node, and the element mapping lands at `0.3.0` | | `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
| `unmappable-image` | an image sits inside other content, or carries a title | give the image a paragraph of its own and drop the title | | `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title |
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.3.0`: Emitting — `adfToMarkdown`, and `adfToHtml` at `0.2.0`:
| Code | Fires when | What you can do | | Code | Fires when | What you can do |
| --- | --- | --- | | --- | --- | --- |
@@ -99,59 +196,61 @@ emit refuses:
| --- | --- | --- | | --- | --- | --- |
| `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip | | `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip |
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span | | `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
| `unspellable-link` | a link `href` or `title` holds what no canonical escape spells — a backslash, a newline, a control character, an entity reference, an angle bracket beside a space | percent-encode the destination (`%5C` for the backslash, `%26` for the `&` that opens the entity), or drop the title |
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines | | `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it | | `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body | | `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
## The guarantees ## The guarantees
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely Serves Goals 1, 3 and 4.
(AGENTS.md §3).
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML below, with three - `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects
carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed as `JSON.parse` builds them — every key and value as `doc` holds it, adjacent text nodes, an
(escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own empty `attrs`, `content` or `marks` and `-0` included, and unknown node types carried opaquely
title-less paragraph; mid-text and titled images are error results. Converting back yields the ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
library's canonical spelling, which round-trips byte-identically — where it converts back at - Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from
all: a parse succeeding is no promise of that, so keep the source until the way back succeeds. `0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
`[a](/a\b)`, `<http://x?a=1&amp;b=2>` and `[a](/x&#10;y)` read cleanly and then refuse. name every exception.
- Three CommonMark spellings parse without an error and build a document the reference renders - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
continuing past a marker change stays one list against CommonMark's two, and a shortcut and a code fence whose info string opens `adf:`, are claimed (each can be kept literal —
reference matching its definition only under Unicode case folding stays unresolved. Each is `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
pinned `pending` in `corpus/commonmark-spec/exceptions.json`. mid-text and titled images are error results, save an image inside another's description, which
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a flattens into the alt text. Converting back yields the library's canonical spelling, which
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.0`. round-trips byte-identically — where it converts back at all: a parse succeeding is no promise of
that, so keep the source until the way back succeeds. ``` ` `` ` ``` reads cleanly and then
refuses.
- Four CommonMark spellings parse without an error and build a document the reference
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
empty link, a list continuing past a marker change stays one list against CommonMark's two, a
shortcut reference matching its definition only under Unicode case folding stays unresolved, and
a link whose text holds an autolink keeps the inner link and leaves the outer brackets literal
text, which the spec requires and the reference itself breaks, nesting one `<a>` in the other.
The first three are pinned `pending` in `corpus/commonmark-spec/exceptions.json`; the suite
holds no example of the fourth.
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding - Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
a carriage return, a link destination or title no canonical escape spells, a paragraph line a carriage return, or a paragraph line beginning with a code span whose backticks read back as a
beginning with a code span whose backticks read back as a fence. Show the refusal and keep the fence. Show the refusal and keep the document read-only; saving markdown you could not produce
document read-only; saving markdown you could not produce is the loss the round-trip exists to is the loss the round-trip exists to stop.
stop.
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error - The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
result rather than the prose it reads as, and an alignment colon in the delimiter row is an result rather than the prose it reads as, and an alignment colon in the delimiter row is an
error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in
input. input.
- Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist - Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist
is the `taskList` directive. is the `taskList` directive — `plainMarkdownToAdf` turns the marker into a `taskList`.
- A document nested deeper than 500 levels is an error result, not a stack overflow. - A document nested deeper than 500 levels is an error result, not a stack overflow, and no input
- The emitted formats are semver surface (AGENTS.md §8). makes a call loop forever.
- **`0.3.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides - The emitted formats are semver surface
`data-*` attributes. Foreign HTML maps a documented element set, an unmappable element is an ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)).
error, and well-formed HTML only — no tag-soup recovery. - **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` deep-equals `doc`; fidelity HTML cannot express rides
`data-*` attributes. Foreign HTML maps a documented element set, which markdown's raw HTML reads
## Who it is for through as well, and a construct outside it is an error; well-formed HTML only — no tag-soup
recovery.
Personas, never named consumers (AGENTS.md §7):
- **Viewer/editor app** — shows a document, lets a human edit, posts back. Losslessness above all.
- **Bot posting content** — converts generated markdown to ADF; needs the CommonMark promise.
- **Export/indexing tool** — bulk ADF→markdown/HTML; needs readable output.
- **LLM/agent pipeline** — documents to a model as markdown, edits back; needs the round-trip and
markdown legible to a reader that half-knows the flavour.
## The package ## The package
ESM only, no runtime dependencies, public npmjs. Built JavaScript with `.d.ts` beside it. Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts`
Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs
Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint.
Contract: `AGENTS.md` §5–6. Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any
ES2022 engine to §Public on npm.
+9 -2
View File
@@ -1,11 +1,18 @@
try { try {
const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js') const { adfToMarkdown, isAdfDocument, markdownToAdf } = await import('/dist/index.js')
// WebDriver's JSON reads -0 back as 0, so a document crosses as JSON text with -0 tagged; run.js revives it.
const spelled = (result) => (result.ok ? { ok: true, value: JSON.stringify(result.value, (_, value) => (Object.is(value, -0) ? '\u0000-0' : value)) } : result)
window.convertCorpus = (corpus) => ({ window.convertCorpus = (corpus) => ({
errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })), errors: corpus.errors.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })),
normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: markdownToAdf(markdown) })), normalization: corpus.normalization.map(({ markdown, name }) => ({ name, parsed: spelled(markdownToAdf(markdown)) })),
realPayloads: corpus.realPayloads.map(({ json, name }) => {
const adf = JSON.parse(json)
const emitted = adfToMarkdown(adf)
return { emitted, isDocument: isAdfDocument(adf), name, parsed: emitted.ok ? spelled(markdownToAdf(emitted.value)) : undefined }
}),
roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => { roundTrip: corpus.roundTrip.map(({ json, markdown, name }) => {
const adf = JSON.parse(json) const adf = JSON.parse(json)
return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: markdownToAdf(markdown) } return { emitted: adfToMarkdown(adf), isDocument: isAdfDocument(adf), name, parsed: spelled(markdownToAdf(markdown)) }
}), }),
}) })
} catch (cause) { } catch (cause) {
+21 -6
View File
@@ -1,8 +1,7 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs'
import { createServer } from 'node:http' import { createServer } from 'node:http'
import { extname, join } from 'node:path' import { extname, join } from 'node:path'
import { toEditorNormal } from '../dist/adf/editor-normal.js' import { readFileSync, readdirSync } from 'node:fs'
const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' } const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' }
const driver = 'http://127.0.0.1:4444' const driver = 'http://127.0.0.1:4444'
@@ -50,6 +49,11 @@ function fixtureNames(kind, extension) {
.sort() .sort()
} }
// convert-corpus.js tags -0 so it survives WebDriver's JSON.
function revived(text) {
return JSON.parse(text, (_, value) => (value === '\u0000-0' ? -0 : value))
}
function refusal(result) { function refusal(result) {
return result.ok ? '' : `${result.error.code}: ${result.error.message}` return result.ok ? '' : `${result.error.code}: ${result.error.message}`
} }
@@ -67,6 +71,7 @@ async function whenDriverAnswers() {
const corpus = { const corpus = {
errors: fixtureNames('errors', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })), errors: fixtureNames('errors', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })), normalization: fixtureNames('normalization', '.md').map((name) => ({ markdown: fixture(name, '.md'), name })),
realPayloads: fixtureNames('real-payloads', '.json').map((name) => ({ json: fixture(name, '.json'), name })),
roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })), roundTrip: fixtureNames('round-trip', '.json').map((name) => ({ json: fixture(name, '.json'), markdown: fixture(name, '.md'), name })),
} }
assert.deepEqual( assert.deepEqual(
@@ -75,7 +80,7 @@ assert.deepEqual(
.map((entry) => entry.name) .map((entry) => entry.name)
.filter((name) => name !== 'commonmark-spec') .filter((name) => name !== 'commonmark-spec')
.sort(), .sort(),
['errors', 'normalization', 'round-trip'], ['errors', 'normalization', 'real-payloads', 'round-trip'],
'a corpus kind the browser leg does not convert', 'a corpus kind the browser leg does not convert',
) )
for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`) for (const [kind, fixtures] of Object.entries(corpus)) assert.ok(fixtures.length > 0, `the browser leg found no ${kind} fixture to convert`)
@@ -106,7 +111,7 @@ for (const [index, { json, markdown, name }] of corpus.roundTrip.entries()) {
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`) assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
assert.equal(result.emitted.value, markdown) assert.equal(result.emitted.value, markdown)
assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`) assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`)
assert.deepEqual(toEditorNormal(result.parsed.value), JSON.parse(json)) assert.deepEqual(revived(result.parsed.value), JSON.parse(json))
}) })
} }
@@ -114,7 +119,17 @@ for (const [index, { name }] of corpus.normalization.entries()) {
const result = results.normalization[index] const result = results.normalization[index]
checking(name, result, () => { checking(name, result, () => {
assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`) assert.ok(result.parsed.ok, `it did not parse — ${refusal(result.parsed)}`)
assert.deepEqual(toEditorNormal(result.parsed.value), JSON.parse(fixture(name, '.json'))) assert.deepEqual(revived(result.parsed.value), JSON.parse(fixture(name, '.json')))
})
}
for (const [index, { json, name }] of corpus.realPayloads.entries()) {
const result = results.realPayloads[index]
checking(name, result, () => {
assert.ok(result.isDocument, `${name}.json is no ADF document`)
assert.ok(result.emitted.ok, `it did not emit — ${refusal(result.emitted)}`)
assert.ok(result.parsed.ok, `it did not parse back — ${refusal(result.parsed)}`)
assert.deepEqual(revived(result.parsed.value), JSON.parse(json))
}) })
} }
@@ -127,5 +142,5 @@ for (const [index, { name }] of corpus.errors.entries()) {
} }
console.log( console.log(
`Firefox ${session.capabilities.browserVersion} converted ${corpus.roundTrip.length} round-trip, ${corpus.normalization.length} normalization and ${corpus.errors.length} error fixtures`, `Firefox ${session.capabilities.browserVersion} converted ${corpus.roundTrip.length} round-trip, ${corpus.normalization.length} normalization and ${corpus.errors.length} error fixtures and ${corpus.realPayloads.length} real payloads`,
) )
+20 -17
View File
@@ -3,28 +3,31 @@ set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
source ./docker-runner.sh source ./docker-runner.sh
in_image "$node_image" npm ci leg "install ($node_image)" in_image "$node_image" npm ci
in_image "$node_image" npm run typecheck leg "typecheck ($node_image)" in_image "$node_image" npm run typecheck
leg "size ratchet ($node_image)" in_image "$node_image" npm run size-ratchet
if ! test_output=$(in_image "$node_image" npm test 2>&1); then test_log=$(mktemp)
printf '%s\n' "$test_output" node_tests() {
exit 1 in_image "$node_image" npm test 2>&1 | tee "$test_log"
fi }
printf '%s\n' "$test_output"
if printf '%s' "$test_output" | grep -q 'ℹ tests 0'; then leg "tests ($node_image)" node_tests
if grep -q 'ℹ tests 0' "$test_log"; then
echo 'the gate ran zero tests — failing instead of a vacuous green' echo 'the gate ran zero tests — failing instead of a vacuous green'
exit 1 exit 1
fi fi
rm -f "$test_log"
in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/ leg "tests ($deno_image)" in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
in_image "$bun_image" bun test src/ leg "tests ($bun_image)" in_image "$bun_image" bun test src/
in_image "$node_image" npm run build leg "build ($node_image)" in_image "$node_image" npm run build
in_image "$node_image" sh -c 'set -e leg "pack and install the tarball ($node_image)" in_image "$node_image" sh -c 'set -e
rm -rf package-tests/node_modules rm -rf package-tests/node_modules
npm pack --pack-destination /tmp >/dev/null npm pack --pack-destination /tmp
npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz >/dev/null' npm install --no-audit --no-fund --no-package-lock --no-save --offline --prefix package-tests /tmp/*.tgz'
in_image "$node_image" npx tsc -p package-tests leg "typecheck the consumer ($node_image)" in_image "$node_image" npx tsc -p package-tests
in_image "$floor_image" node package-tests/node-floor.js leg "round-trip on the engines floor ($floor_image)" in_image "$floor_image" node package-tests/node-floor.js
with_firefox in_image "$node_image" node browser-tests/run.js leg "browser ($firefox_image)" with_firefox in_image "$node_image" node browser-tests/run.js
+12 -12
View File
@@ -1,26 +1,26 @@
# The corpus # The corpus
One directory per contract kind, each landing with its milestone: One directory per contract kind:
- `round-trip/` — `<name>.json` + `<name>.md`: the markdown `adfToMarkdown` must emit for that - `round-trip/` — `<name>.json` + `<name>.md`: the markdown `adfToMarkdown` must emit for that
document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped document, byte for byte, and that `markdownToAdf` must read back to it (`docs/decisions.md` §The
by what the fixture exercises. round-trip is the product). Grouped by what the fixture exercises.
- `normalization/` — `<name>.md` + `<name>.json`: markdown input, and the document - `normalization/` — `<name>.md` + `<name>.json`: markdown input, and the document
`markdownToAdf` must build from it, which must in turn emit and read back to itself. The `markdownToAdf` must build from it, which must in turn emit and read back to itself. The
markdown is not canonical. markdown is not canonical.
- `errors/` — `<name>.md`: markdown input that must not convert. A `<name>.error` beside it - `errors/` — `<name>.md`: markdown input that must not convert. A `<name>.error` beside it
pins which error. pins which error.
- `real-payloads/` — `<name>.json`: sanitized live ADF, round-tripped ADF→markdown→ADF. No - `real-payloads/` — `<name>.json`: ADF Atlassian's editor wrote, round-tripped ADF→markdown→ADF.
expected markdown. No expected markdown.
- `commonmark-spec/` — the CommonMark suite run against `markdownToAdf` by three checks. `spec.json` - `commonmark-spec/` — the CommonMark suite run against `markdownToAdf` by three checks. `spec.json`
is the suite; `refusals.json` pins each refusing example to its error `code`; `exceptions.json` is the suite; `refusals.json` pins each refusing example to its error `code`; `exceptions.json`
pins each known divergence by `check`, `example`, `kind` and the exact `divergence`, with a pins each known divergence by `check`, `example`, `kind` and the exact `divergence`, with a
`reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node `reason`. `kind` is `mark-model` (the permanent count divergence from ADF's mark-per-text-node
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap a later model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
milestone may close).
JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. `spec.json` is the vendored, JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
upstream machine-readable suite, byte-exact from (`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John suite, byte-exact from [spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json)
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not (CommonMark 0.31.2, © John MacFarlane,
re-serialized by the corpus gate. [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not re-serialized by the
corpus gate.
-21
View File
@@ -1,18 +1,4 @@
[ [
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 196,
"kind": "unspellable",
"reason": "The link title holds literal newlines no escape spells."
},
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 202,
"kind": "unspellable",
"reason": "The link destination holds a backslash the flavour cannot escape."
},
{ {
"check": "count", "check": "count",
"divergence": "ul 2/1", "divergence": "ul 2/1",
@@ -349,13 +335,6 @@
"kind": "pending", "kind": "pending",
"reason": "An empty link text stays literal text; CommonMark renders an empty link." "reason": "An empty link text stays literal text; CommonMark renders an empty link."
}, },
{
"check": "fixpoint",
"divergence": "unspellable-link",
"example": 502,
"kind": "unspellable",
"reason": "The link destination holds a backslash the flavour cannot escape."
},
{ {
"check": "count", "check": "count",
"divergence": "a 1/5 em 1/4", "divergence": "a 1/5 em 1/4",
+2 -2
View File
@@ -1,2 +1,2 @@
:::tableCell {colwidth="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"} !adf:tableCell {colwidth="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
::: !adf:/tableCell
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,5 @@
```adf:
{
"type": "blockCard"
}
```
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,3 @@
```adf:\\
{}
```
+1
View File
@@ -0,0 +1 @@
unsupported-node-shape
+5
View File
@@ -0,0 +1,5 @@
```adf:blockCard
{
"type": "blockCard"
}
```
+1 -1
View File
@@ -1 +1 @@
_a :adf{json="{\"type\":\"blockCard\"}"} b_ _a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
+1 -1
View File
@@ -1 +1 @@
:adf{json="{"} !adf:carry{json="{"}
+2 -2
View File
@@ -1,3 +1,3 @@
```adf ```adf:blockCard
{"type": {"attrs":
``` ```
+1 -1
View File
@@ -1 +1 @@
:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"} !adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
+1 -1
View File
@@ -1,2 +1,2 @@
> Part. > Part.
::: !adf:/panel
+2 -2
View File
@@ -1,3 +1,3 @@
:::codeBlock {wrap=true} !adf:codeBlock {wrap=true}
fn main() {} fn main() {}
::: !adf:/codeBlock
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,8 @@
!adf:codeBlock {attrs=empty}
```js
a
```
```js
b
```
!adf:/codeBlock
@@ -0,0 +1 @@
unsupported-node-shape
+7
View File
@@ -0,0 +1,7 @@
!adf:codeBlock
```
a
```
```
```
!adf:/codeBlock
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,8 @@
!adf:codeBlock
```js
a
```
```ts
b
```
!adf:/codeBlock
+2 -2
View File
@@ -1,5 +1,5 @@
:::codeBlock {language=rust wrap=true} !adf:codeBlock {language=rust wrap=true}
``` ```
fn main() {} fn main() {}
``` ```
::: !adf:/codeBlock
-1
View File
@@ -1 +0,0 @@
:::
-3
View File
@@ -1,3 +0,0 @@
:::adf
{"type":"rule"}
:::
+1 -1
View File
@@ -1 +1 @@
::media {type=file id=a-1} !adf:media {type=file id=a-1}
+2 -2
View File
@@ -1,3 +1,3 @@
:::panel info {bogus=1} !adf:panel info {bogus=1}
Part. Part.
::: !adf:/panel
+1 -1
View File
@@ -1 +1 @@
unsupported-node-shape malformed-directive
+2 -2
View File
@@ -1,3 +1,3 @@
:::rule !adf:media {id=01a0aa6b-4221-7518-a640-2b97d6217ce2 type=file}
Part. Part.
::: !adf:/media
@@ -0,0 +1,3 @@
!adf:carry
{"type":"rule"}
!adf:/carry
@@ -0,0 +1,4 @@
!adf:panel info
!adf:expand
Part.
!adf:/panel
@@ -0,0 +1,2 @@
!adf:panel info
Part.
+1 -1
View File
@@ -1 +1 @@
Part :mention[**@Mikael**]{id=b1c2}. Part !adf:mention[**@Mikael**]{id=b1c2}.
@@ -1,2 +0,0 @@
:::panel info
Part.
+1 -1
View File
@@ -1 +1 @@
::rule !adf:rule
+1 -1
View File
@@ -1 +1 @@
Part :mention[@A and more. Part !adf:mention[@A and more.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
See !adf:link[the docs]{href="https://example.com/docs"} here.
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
See !adf:link[the docs]{title="Setup guide"} here.
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
See !adf:link[<https://example.com/docs>]{collection=contentId-98237 href="/docs"} here.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
!adf:link[the docs]{href="https://example.com/docs"}
+1 -1
View File
@@ -1 +1 @@
Part :em[emphasised] here. Part !adf:em[emphasised] here.
+2 -2
View File
@@ -1,3 +1,3 @@
:::widget info !adf:widget info
Part. Part.
::: !adf:/widget
@@ -0,0 +1 @@
malformed-directive
+1
View File
@@ -0,0 +1 @@
Part !adf: here.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
!adf:status[!adf:carry{json="{\"attrs\":{\"localId\":\"x\"},\"text\":\"ok\",\"type\":\"text\"}"}]{color=red}
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
!adf:status[!adf:carry{json="{\"marks\":[],\"text\":\"x\",\"type\":\"text\"}"}]
+1 -1
View File
@@ -1 +1 @@
Part :text{text=plain} here. Part !adf:text{text=plain} here.
+1 -1
View File
@@ -1 +1 @@
::rule {localId="a-1"} !adf:rule {localId="a-1"}
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
!adf:doc
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,3 @@
!adf:doc {content=none}
Text.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1,3 @@
!adf:panel info {attrs=empty}
Text.
!adf:/panel
@@ -0,0 +1 @@
unsupported-node-shape
+3
View File
@@ -0,0 +1,3 @@
!adf:paragraph {content=empty}
Text.
!adf:/paragraph
+1
View File
@@ -0,0 +1 @@
unsupported-node-shape
+3
View File
@@ -0,0 +1,3 @@
!adf:paragraph {attrs=none}
Text.
!adf:/paragraph
+1
View File
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
**!adf:date{marks=empty}**
+1
View File
@@ -0,0 +1 @@
malformed-directive
+7
View File
@@ -0,0 +1,7 @@
- Bolt M8
!adf:listBreak
Packed.
!adf:/listBreak
- Nut M8
+1 -1
View File
@@ -1,5 +1,5 @@
- Bolt M8 - Bolt M8
::listBreak !adf:listBreak
Packed. Packed.
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
a!adf:textBreak{x=y}b
+1
View File
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
a!adf:textBreak[x]b
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
**a**!adf:textBreak{}b
+1
View File
@@ -0,0 +1 @@
unsupported-node-shape
+1
View File
@@ -0,0 +1 @@
!adf:textBreak{}Hello
+107
View File
@@ -0,0 +1,107 @@
{
"content": [
{
"content": [
{
"text": "[",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://example.com/"
},
"type": "link"
}
],
"text": "https://example.com/",
"type": "text"
},
{
"text": "](/v)",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "[a",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://example.com/b"
},
"type": "link"
}
],
"text": "https://example.com/b",
"type": "text"
},
{
"text": "c](/v)",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "[",
"type": "text"
},
{
"marks": [
{
"attrs": {
"collection": "contentId-98237",
"href": "/docs"
},
"type": "link"
}
],
"text": "the docs",
"type": "text"
},
{
"text": "](/v)",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "[",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "/u"
},
"type": "link"
}
],
"text": "a",
"type": "text"
},
{
"text": "](/v)",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+7
View File
@@ -0,0 +1,7 @@
[<https://example.com/>](/v)
[a<https://example.com/b>c](/v)
[!adf:link[the docs]{collection=contentId-98237 href="/docs"}](/v)
[[a](/u)](/v)
@@ -0,0 +1,57 @@
{
"content": [
{
"content": [
{
"marks": [
{
"attrs": {
"href": "/a\\b"
},
"type": "link"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"attrs": {
"href": "http://x?a=1&amp;b=2"
},
"type": "link"
}
],
"text": "http://x?a=1&amp;b=2",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"attrs": {
"href": "/x\ny"
},
"type": "link"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,5 @@
[a](/a\b)
<http://x?a=1&amp;b=2>
[a](/x&#10;y)
+173
View File
@@ -0,0 +1,173 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "0d42e7ff24ba"
},
"content": [
{
"text": "Code blocks",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "a971506228e7"
},
"content": [
{
"text": "Run the setup once, then start the server with ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "npm start",
"type": "text"
},
{
"text": " and open the page.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "javascript",
"localId": "eaf42046-6454-451d-ab13-8bd5ed2333bb",
"wrap": true
},
"content": [
{
"text": "function greet(name) {\n return `Hello, ${name}!`\n}",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "d69616dee641"
},
"content": [
{
"text": "The same idea in Python:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "python",
"localId": "2094b559-280a-49d5-9be3-182572e5d2d8",
"wrap": true
},
"content": [
{
"text": "def total(prices):\n return sum(prices)\n\nprint(total([3, 4.5]))",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "abe1971d1f18"
},
"content": [
{
"text": "A query that lists visitors by city:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "sql",
"localId": "5551dc8e-8fe0-455e-a9cf-22e48b589fe8",
"wrap": true
},
"content": [
{
"text": "SELECT name, city\nFROM visitors\nWHERE city = 'Lund'\nORDER BY name;",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "51efdd2f1f02"
},
"content": [
{
"text": "Plain output, no language chosen:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "d0f83c92-c252-4a70-9e31-dfb5ac255cd1",
"wrap": true
},
"content": [
{
"text": "$ ./setup\nChecking folders... done\n 3 files copied",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "3dac57e146fa"
},
"content": [
{
"text": "Keep ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": ".env",
"type": "text"
},
{
"text": " files out of version control, and never paste a ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "password",
"type": "text"
},
{
"text": " into a ticket.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+254
View File
@@ -0,0 +1,254 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "0fcbc98aec1f"
},
"content": [
{
"text": "Where things stand",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "32b6266eb05c"
},
"content": [
{
"text": "Office move ",
"type": "text"
},
{
"attrs": {
"color": "blue",
"localId": "1ab19511-a0fa-41a1-b6d0-6729813c0526",
"style": "mixedCase",
"text": "In progress"
},
"type": "status"
},
{
"text": " since ",
"type": "text"
},
{
"attrs": {
"localId": "493624162706",
"timestamp": "1790812800000"
},
"type": "date"
},
{
"text": " , owned by ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "604060e1-e533-4019-aa28-21952dfdea19",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " .",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "73a94328c89f"
},
"content": [
{
"text": "Furniture ",
"type": "text"
},
{
"attrs": {
"color": "green",
"localId": "7b27ff33-1483-452b-8982-311cf9429023",
"style": "mixedCase",
"text": "Done"
},
"type": "status"
},
{
"text": " , cabling ",
"type": "text"
},
{
"attrs": {
"color": "red",
"localId": "b6eb05a9-e0e7-4269-9cbd-e912640364a6",
"style": "mixedCase",
"text": "Blocked"
},
"type": "status"
},
{
"text": " , signage ",
"type": "text"
},
{
"attrs": {
"color": "yellow",
"localId": "ff7ed7fb-1846-48cb-b5ea-8be6b598ccc6",
"style": "mixedCase",
"text": "Waiting"
},
"type": "status"
},
{
"text": " , parking ",
"type": "text"
},
{
"attrs": {
"color": "purple",
"localId": "e9218ff4-6fcb-4e43-b36a-b1bef8a19376",
"style": "mixedCase",
"text": "Maybe"
},
"type": "status"
},
{
"text": " and keys ",
"type": "text"
},
{
"attrs": {
"color": "neutral",
"localId": "f1455340-2469-4bc6-a371-ed6b1f54386c",
"style": "mixedCase",
"text": "Not started"
},
"type": "status"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "8d9e2dc70265"
},
"content": [
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "ab5b7997-43ce-4238-b540-db3864e0965c",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " please confirm the handover on ",
"type": "text"
},
{
"attrs": {
"localId": "77e2bdefa7fd",
"timestamp": "1795132800000"
},
"type": "date"
},
{
"text": " before lunch.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "f7fb8a7527a3"
},
"content": [
{
"text": "Great work so far ",
"type": "text"
},
{
"attrs": {
"id": "1f44d",
"localId": "8fe9a02e94ce",
"shortName": ":thumbsup:",
"text": "👍"
},
"type": "emoji"
},
{
"text": " and thanks for the snacks ",
"type": "text"
},
{
"attrs": {
"id": "1f389",
"localId": "c19496cc742f",
"shortName": ":tada:",
"text": "🎉"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
},
{
"attrs": {
"id": "1f600",
"localId": "c05a6757d0ee",
"shortName": ":grinning:",
"text": "😀"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b75918031ebd"
},
"content": [
{
"text": "Deliveries go to:",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Building C, third floor",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Ring the bell twice",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+527
View File
@@ -0,0 +1,527 @@
{
"content": [
{
"attrs": {
"level": 1,
"localId": "cfdcc54363d9"
},
"content": [
{
"text": "Project handbook",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "91177a27b0ff"
},
"content": [
{
"text": "Welcome to the handbook for the spring project.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"level": 2,
"localId": "7e7d9113a89b"
},
"content": [
{
"text": "Getting started",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "e65711ca96b6"
},
"content": [
{
"text": "Pack the following before the first day:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b5c36c57fe79"
},
"content": [
{
"attrs": {
"localId": "1f4aa02de452"
},
"content": [
{
"attrs": {
"localId": "8993e22a13aa"
},
"content": [
{
"text": "Laptop",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "2b335a4504ab"
},
"content": [
{
"attrs": {
"localId": "c9180347caf0"
},
"content": [
{
"attrs": {
"localId": "60990e48da32"
},
"content": [
{
"text": "Charger",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "fc5d4c8a875d"
},
"content": [
{
"attrs": {
"localId": "3b8ec5318463"
},
"content": [
{
"text": "Mouse",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "90f5f37a8a8f"
},
"content": [
{
"attrs": {
"localId": "90fab5a310c5"
},
"content": [
{
"text": "Notebook",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "46840b8bd24b",
"order": 1
},
"content": [
{
"attrs": {
"localId": "b4f06fbd15ef"
},
"content": [
{
"attrs": {
"localId": "93db4583bdba"
},
"content": [
{
"text": "Lined for notes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "6904c80dd06d"
},
"content": [
{
"attrs": {
"localId": "0bc1c819d38d"
},
"content": [
{
"text": "Squared for sketches",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "65bd0afce26e"
},
"content": [
{
"attrs": {
"localId": "a753e5ecdad3"
},
"content": [
{
"text": "Water bottle",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"level": 3,
"localId": "5b6a671499c5"
},
"content": [
{
"text": "Unpacking",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "f460abd3a962"
},
"content": [
{
"text": "The first two steps happen at reception, so ours start at three:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "baf0046a0432",
"order": 3
},
"content": [
{
"attrs": {
"localId": "b4df865a31a9"
},
"content": [
{
"attrs": {
"localId": "3f1b32f952d0"
},
"content": [
{
"text": "Unpack the boxes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "f64930d87d4e"
},
"content": [
{
"attrs": {
"localId": "d953389c865a"
},
"content": [
{
"text": "Label the shelves",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "840daad9cb43"
},
"content": [
{
"attrs": {
"localId": "8708b8c7aeec"
},
"content": [
{
"text": "Recycle the cardboard",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"level": 4,
"localId": "27d1d408ae3e"
},
"content": [
{
"text": "Shelf labels",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "8b5cf239dbb5",
"order": 1
},
"content": [
{
"attrs": {
"localId": "0948562abdc3"
},
"content": [
{
"attrs": {
"localId": "4da9d4f45923"
},
"content": [
{
"text": "Top shelf",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "36edf97d8e2c"
},
"content": [
{
"attrs": {
"localId": "7915e252429c"
},
"content": [
{
"attrs": {
"localId": "9b15cc9fa8a1"
},
"content": [
{
"text": "Binders",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "efe607ab9e41"
},
"content": [
{
"attrs": {
"localId": "d559a2f29321"
},
"content": [
{
"text": "Archive boxes",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "229692097876"
},
"content": [
{
"attrs": {
"localId": "928b1862c273"
},
"content": [
{
"text": "Bottom shelf",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"level": 5,
"localId": "353a84ef9126"
},
"content": [
{
"text": "Quiet hours",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "9c11b949ed48"
},
"content": [
{
"attrs": {
"localId": "bd576489e8b6"
},
"content": [
{
"text": "Good tools make good work, and quiet rooms make good thinking.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "727fda576a9d"
},
"content": [
{
"text": "Please keep calls in the booths.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "blockquote"
},
{
"attrs": {
"level": 6,
"localId": "b12277440bc7"
},
"content": [
{
"text": "Small print",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "9f6fbb5a19af"
},
"type": "rule"
},
{
"attrs": {
"localId": "8a6e253c110e"
},
"content": [
{
"text": "Office hours:",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Monday to Thursday, eight to five",
"type": "text"
},
{
"type": "hardBreak"
},
{
"text": "Friday, eight to three",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b9c3f5daa373"
},
"content": [
{
"text": "That is all for now.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+106
View File
@@ -0,0 +1,106 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "d5c3a2503683"
},
"content": [
{
"text": "Media and attachments",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "f71287760586"
},
"content": [
{
"text": "A photo of the meadow behind the office:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"layout": "align-start",
"localId": "95de25bcdecc",
"width": 320,
"widthType": "pixel"
},
"content": [
{
"attrs": {
"alt": "Green meadow under a yellow sun",
"collection": "",
"height": 180,
"id": "8c586460-fb0d-44f6-949f-d800d5dd0f45",
"localId": "630d0d0457b8",
"type": "file",
"width": 320
},
"marks": [
{
"attrs": {
"color": "#091e4224",
"size": 2
},
"type": "border"
}
],
"type": "media"
}
],
"type": "mediaSingle"
},
{
"attrs": {
"localId": "b4bfee3ad3ec"
},
"type": "paragraph"
},
{
"attrs": {
"localId": "ffe5710ea995"
},
"content": [
{
"text": "The packing list is attached as a file:",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"attrs": {
"collection": "",
"id": "8dfd7046-2382-4377-b3ba-ec31e34cd01a",
"type": "file"
},
"type": "media"
}
],
"type": "mediaGroup"
},
{
"attrs": {
"localId": "77fcb609f709"
},
"content": [
{
"text": "Both were uploaded from the editor.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,292 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "d483ec27f809"
},
"content": [
{
"text": "Panels",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "cb02f8e9812b"
},
"content": [
{
"text": "Each colour of panel below holds a short note for the visiting team.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "b6d76c21e77e",
"panelType": "info"
},
"content": [
{
"attrs": {
"localId": "2d871d80f6e1"
},
"content": [
{
"text": "The workshop starts at nine in the morning.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "cbaab515445c",
"panelType": "note"
},
"content": [
{
"attrs": {
"localId": "13cafc26c42d"
},
"content": [
{
"text": "Bring your own laptop charger.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "c5fa8b34e2aa",
"panelType": "success"
},
"content": [
{
"attrs": {
"localId": "e186fca354d9"
},
"content": [
{
"text": "All rooms are booked for the week.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "ee223a0f886e",
"panelType": "warning"
},
"content": [
{
"attrs": {
"localId": "e0006fb2b972"
},
"content": [
{
"text": "The east entrance closes early on Fridays.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ea6a56b9c863"
},
"content": [
{
"text": "Use the main door after four.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"localId": "fedbadc6d1c7",
"panelType": "error"
},
"content": [
{
"attrs": {
"localId": "c0f6122d81b7"
},
"content": [
{
"text": "The projector in room two is out of order.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"level": 2,
"localId": "afe638de11c5"
},
"content": [
{
"text": "Expands",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "e635bf2eac43",
"title": "Agenda for the first day"
},
"content": [
{
"attrs": {
"localId": "c8beb4c392ee"
},
"content": [
{
"attrs": {
"localId": "6d6ff472d878"
},
"content": [
{
"attrs": {
"localId": "d1186101c5a1"
},
"content": [
{
"text": "Welcome and introductions",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "01d203b97343"
},
"content": [
{
"attrs": {
"localId": "61bfec91218d"
},
"content": [
{
"text": "Tour of the building",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "004e47604c2a"
},
"content": [
{
"attrs": {
"localId": "726aa40e1e38"
},
"content": [
{
"text": "Lunch in the courtyard",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "expand"
},
{
"attrs": {
"localId": "cb6e48b9d746"
},
"content": [
{
"text": "Questions go to the organisers before the event.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "32a8832682b2",
"title": "Travel details"
},
"content": [
{
"attrs": {
"localId": "00b05814cccb"
},
"content": [
{
"text": "Trains leave the central station every twenty minutes.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "eec0df0ed4f1",
"panelType": "note"
},
"content": [
{
"attrs": {
"localId": "712c49fa7ecc"
},
"content": [
{
"text": "Tickets are cheaper before seven.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
}
],
"type": "expand"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,630 @@
{
"content": [
{
"attrs": {
"localId": "3b911072ce78"
},
"content": [
{
"text": "Summary of the planning session on Thursday. Attendees: ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "c8b64b9f-d396-4525-9456-38cf690ade1e",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " and two guests from the design team.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"level": 2,
"localId": "3e97ef36cbc3"
},
"content": [
{
"text": "Where we are",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "530e0e3aa621"
},
"content": [
{
"text": "The beta is ",
"type": "text"
},
{
"attrs": {
"color": "green",
"localId": "ac0975eb-fafe-44ad-8293-8a12fdbcaebc",
"style": "mixedCase",
"text": "On track"
},
"type": "status"
},
{
"text": " and the public launch is set for ",
"type": "text"
},
{
"attrs": {
"localId": "84d9a1cde860",
"timestamp": "1793664000000"
},
"type": "date"
},
{
"text": " .",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ffcd53578640",
"panelType": "warning"
},
"content": [
{
"attrs": {
"localId": "0f0e8b9e13bb"
},
"content": [
{
"text": "Translation files are still ",
"type": "text"
},
{
"marks": [
{
"type": "strong"
}
],
"text": "missing",
"type": "text"
},
{
"text": " for two languages.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "panel"
},
{
"attrs": {
"level": 2,
"localId": "d9b590d479ad"
},
"content": [
{
"text": "Decisions",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "69dc2d52-34f7-43bb-8d6c-36d52a47179e"
},
"content": [
{
"attrs": {
"localId": "852a35a7-f660-4161-85dd-2d54adaf662b",
"state": "DECIDED"
},
"content": [
{
"text": "Ship the beta to internal users first",
"type": "text"
}
],
"type": "decisionItem"
},
{
"attrs": {
"localId": "2df16457-331f-4d26-90ba-539f82949d70",
"state": "DECIDED"
},
"content": [
{
"text": "Keep the old export for one more release",
"type": "text"
}
],
"type": "decisionItem"
}
],
"type": "decisionList"
},
{
"attrs": {
"level": 2,
"localId": "7813370b5e89"
},
"content": [
{
"text": "Next steps",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "21a7a33c-199d-4396-a148-c8404f70554f"
},
"content": [
{
"attrs": {
"localId": "5343dfdb-e70d-4ce6-bca5-8a382123c7bd",
"state": "TODO"
},
"content": [
{
"text": "Draft the release notes",
"type": "text"
}
],
"type": "taskItem"
},
{
"attrs": {
"localId": "67d65eb8-17e9-4cd6-93cb-7d495f84e01d",
"state": "TODO"
},
"content": [
{
"text": "Book the demo room",
"type": "text"
}
],
"type": "taskItem"
}
],
"type": "taskList"
},
{
"attrs": {
"level": 2,
"localId": "e5806b037d61"
},
"content": [
{
"text": "Numbers",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"isNumberColumnEnabled": false,
"layout": "align-start",
"localId": "cbae6bea-3c35-42b9-9a8f-3b37749e1a87"
},
"content": [
{
"attrs": {
"localId": "e4d343447bf5"
},
"content": [
{
"attrs": {
"localId": "4fcec7ad0a0f"
},
"content": [
{
"attrs": {
"localId": "974849d6e63c"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Metric",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "42df5203769e"
},
"content": [
{
"attrs": {
"localId": "8b0da0854a0a"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Beta",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"attrs": {
"localId": "cbdae6c8508d"
},
"content": [
{
"attrs": {
"localId": "2584f222d6c3"
},
"content": [
{
"marks": [
{
"type": "strong"
}
],
"text": "Goal",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "65e4d7a700c0"
},
"content": [
{
"attrs": {
"localId": "a37175350eea"
},
"content": [
{
"attrs": {
"localId": "7efe32972497"
},
"content": [
{
"text": "Sign-ups",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "e0a95cdbd3c5"
},
"content": [
{
"attrs": {
"localId": "4628a3afe71b"
},
"content": [
{
"text": "140",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "b802fc652961"
},
"content": [
{
"attrs": {
"localId": "94c6ec93acb2"
},
"content": [
{
"text": "200",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"attrs": {
"localId": "bd6465cc546d"
},
"content": [
{
"attrs": {
"localId": "b6dcef3c6ac5"
},
"content": [
{
"attrs": {
"localId": "ba5c99353d30"
},
"content": [
{
"text": "Crash reports",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "554b7b5a8e71"
},
"content": [
{
"attrs": {
"localId": "569a134488bc"
},
"content": [
{
"text": "3",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"attrs": {
"localId": "fb03db2017b1"
},
"content": [
{
"attrs": {
"localId": "c0ce0ada482c"
},
"content": [
{
"text": "0",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
}
],
"type": "table"
},
{
"attrs": {
"localId": "b4eb1362eb9f"
},
"content": [
{
"text": "The flag that turns the beta on:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"language": "yaml",
"localId": "14298a36-6632-458f-a548-7fccd4b7186b",
"wrap": true
},
"content": [
{
"text": "beta:\n enabled: true\n audience: internal",
"type": "text"
}
],
"type": "codeBlock"
},
{
"attrs": {
"localId": "a98274e22eaa"
},
"content": [
{
"text": "Risks we talked about:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "fb3e387688ff"
},
"content": [
{
"attrs": {
"localId": "fdeee4ce0678"
},
"content": [
{
"attrs": {
"localId": "5ff2e05f1c7e"
},
"content": [
{
"text": "Late translations",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "043a7ded718e"
},
"content": [
{
"attrs": {
"localId": "73a4d8e4ef0d"
},
"content": [
{
"text": "The holiday week",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "3e32c1b868a1"
},
"content": [
{
"attrs": {
"localId": "98969cd5548e"
},
"content": [
{
"attrs": {
"localId": "3b9989413325"
},
"content": [
{
"text": "Half the team is away",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"localId": "ba7d8422f4c6"
},
"content": [
{
"attrs": {
"localId": "a6837cfad3f5"
},
"content": [
{
"text": "Small steps, shipped often.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "blockquote"
},
{
"attrs": {
"localId": "6ec9ef8df224"
},
"content": [
{
"text": "Thanks, everyone ",
"type": "text"
},
{
"attrs": {
"id": "1f389",
"localId": "2481c60576e0",
"shortName": ":tada:",
"text": "🎉"
},
"type": "emoji"
},
{
"text": " ",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "782129bf30c1",
"title": "Raw notes"
},
"content": [
{
"attrs": {
"localId": "e9614e389875"
},
"content": [
{
"text": "We spent twenty minutes on the colour of the launch banner.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "expand"
}
],
"type": "doc",
"version": 1
}
+118
View File
@@ -0,0 +1,118 @@
{
"content": [
{
"attrs": {
"localId": "3e9517d02129"
},
"content": [
{
"text": "Looks good to me. Two small things:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "5738867bf15d",
"order": 1
},
"content": [
{
"attrs": {
"localId": "ccd8d283e36d"
},
"content": [
{
"attrs": {
"localId": "638111edfab4"
},
"content": [
{
"text": "Rename ",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "beta.enabled",
"type": "text"
},
{
"text": " before the launch",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
},
{
"attrs": {
"localId": "0adfb25bb321"
},
"content": [
{
"attrs": {
"localId": "980750fad0fe"
},
"content": [
{
"text": "Move the demo to the ",
"type": "text"
},
{
"marks": [
{
"type": "em"
}
],
"text": "large",
"type": "text"
},
{
"text": " room",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "listItem"
}
],
"type": "orderedList"
},
{
"attrs": {
"localId": "40e080c7acd4"
},
"content": [
{
"text": "Thanks ",
"type": "text"
},
{
"attrs": {
"accessLevel": "",
"id": "712020:78bf5058-bc76-4783-99af-6d70cb593cee",
"localId": "c4e073cd-8b3a-48b2-93a0-ade57b4ec2cf",
"text": "@Larvit Developer"
},
"type": "mention"
},
{
"text": " !",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
+124
View File
@@ -0,0 +1,124 @@
{
"content": [
{
"attrs": {
"level": 2,
"localId": "e632994a13ae"
},
"content": [
{
"text": "Smart links",
"type": "text"
}
],
"type": "heading"
},
{
"attrs": {
"localId": "dcfa975dde87"
},
"content": [
{
"text": "An inline card sits in a sentence, like ",
"type": "text"
},
{
"attrs": {
"localId": "042b9a548674",
"url": "https://en.wikipedia.org/wiki/Markdown"
},
"type": "inlineCard"
},
{
"text": " which keeps the text flowing.",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "c138d931a131"
},
"content": [
{
"text": "A block card stands on its own line:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "9e527ae5f2bd"
},
"type": "paragraph"
},
{
"attrs": {
"localId": "88bd10974ec6",
"url": "https://spec.commonmark.org/"
},
"type": "blockCard"
},
{
"attrs": {
"localId": "9879bb1368ea"
},
"content": [
{
"text": " An embed shows the page itself, where the site allows it:",
"type": "text"
}
],
"type": "paragraph"
},
{
"attrs": {
"localId": "ccbed18139bb"
},
"type": "paragraph"
},
{
"attrs": {
"layout": "wide",
"localId": "8320cf243168",
"originalHeight": 480,
"originalWidth": 853.344,
"url": "https://www.youtube.com/watch?v=jNQXAC9IVRw",
"width": 100
},
"type": "embedCard"
},
{
"attrs": {
"localId": "22f60b236c1c"
},
"content": [
{
"text": " And a plain address stays a link: ",
"type": "text"
},
{
"marks": [
{
"attrs": {
"href": "https://www.markdownguide.org/"
},
"type": "link"
}
],
"text": "https://www.markdownguide.org/",
"type": "text"
},
{
"text": " for reference.",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}

Some files were not shown because too many files have changed in this diff Show More