diff --git a/AGENTS.md b/AGENTS.md index ad414a9..175f93c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,11 +130,10 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma ## 7. The working loop -`todo.md` lists what is left under the release that ships it, in shipping order. A session works -one chunk, starting from the first item under the earliest release, and stops when that chunk -merges, whatever it was asked to finish: a release is a chain of sessions, so an instruction to -work until a release is done names the chain, not the session. An open PR is a chunk already in -flight, and finishing it is the session. +A session works one chunk, starting from the first item under the earliest release in `todo.md`, +and stops when that chunk merges, whatever it was asked to finish: a release is a chain of +sessions, so an instruction to work until a release is done names the chain, not the session. An +open PR is a chunk already in flight, and finishing it is the session. Per chunk: 1. Fresh worktree off updated `origin/main`; implement tests-first (§3). @@ -143,8 +142,7 @@ Per chunk: 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. 3. Merge the PR (standing authorization, this repo only, granted by the maintainer through the - `0.2.0` release), delete the chunk's items from `todo.md` — the part a consumer sees goes to - `CHANGELOG.md`'s `## Unreleased`, reworded for consumers — report, stop. + `0.2.0` release), report, stop. Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the @@ -170,17 +168,10 @@ allow, rendered, shuffled, with no rationale and nothing saying what is implemen 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`. -### Findings, numbers and empty releases +### Stated numbers -- A finding inside the chunk's items is fixed in the chunk. Outside them, a new `todo.md` item, - always in a release, weighed against every item on that release by the personas and - `docs/decisions.md` §Plain markdown is a flavour of the grammar through §Names stay text — an item - it outweighs moves later. A weighing no entry decides is asked as a gap. -- 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. -- An earliest release with no items left and nothing shipped toward it is planned as the chunk: - every later item weighed as above, the order written in `todo.md`, and the maintainer's approval - taken before any code. With work shipped toward it, it is ready to cut: report that and stop. +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 diff --git a/docs/decisions.md b/docs/decisions.md index efc13e3..a4fdce8 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -39,7 +39,7 @@ merges. "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. -Replaced by deep equality with `todo.md` 40 (2026-09-28, the maintainer). +Replaced by deep equality with `todo.md` item 40 (2026-09-28, the maintainer). ## Unknown nodes ride the carry @@ -56,7 +56,7 @@ Where a container's own spelling cannot hold the child it has — a `bulletList` ## Foreign HTML sorts three ways 2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 4. Valid while ADF holds no node -for a bare container, a comment or a script. Lands with `todo.md` 6. +for a bare container, a comment or a script. Lands with `todo.md` item 6. Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent drop of content: diff --git a/spec/flavour.md b/spec/flavour.md index 1917825..fcb97ba 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -177,7 +177,7 @@ In block-directive position `!adf:carry` is a named error — the carry's block CommonMark input may contain raw HTML. `markdownToAdf` routes each construct through the foreign HTML element mapping (`docs/decisions.md` §Foreign HTML sorts three ways; specified with the HTML -dialect, todo.md milestone 6) — ADF has no raw-HTML node, so a construct without a mapping, comments +dialect, `todo.md` item 6) — ADF has no raw-HTML node, so a construct without a mapping, comments and processing instructions included, is an error result naming it. The flavour never emits raw HTML. diff --git a/todo.md b/todo.md index 857eb06..4fb2acc 100644 --- a/todo.md +++ b/todo.md @@ -1,95 +1,150 @@ -# Todo +# todo -## 0.2.0 +## Scoring -- **43 — Give each markdown input its own reader, strict to its own standard.** Today - `markdownToAdf` reads CommonMark and the lossless flavour as one input: text shaped like a - directive, a pipe table or a `~~` pair becomes a flavour node where CommonMark reads plain text - (Goals 3 and 4). A caller names the markdown it hands in: CommonMark, read as its spec says, or - the lossless flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a - caller takes. -- **45 — Replace `isAdfDocument` with a reader returning `Result`.** Goal 1 has every - call return a result; the boolean guard is the one export that does not, and it cannot say which - branch refused, where `not-an-adf-document`'s message already does. Breaking: `MIGRATION.md` - shows the guard's replacement. -- **40 — Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes.** - Today it holds for editor-normal documents only: two adjacent text nodes with the same marks - merge, an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines - and bots build. Spell each so it reads back as written, CommonMark's spelling kept wherever none - occurs; the spellings are part of the chunk. `docs/decisions.md` §Equality is editor-normal, - `spec/flavour.md` and `corpus/README.md` follow, and the tests drop `toEditorNormal`. -- **6 — Specify the HTML dialect.** Element-by-element mapping, the `data-*` fidelity scheme, the - opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts — the set - `markdownToAdf` shares (`spec/flavour.md` §Raw HTML in input). - The set sorts per `docs/decisions.md` §Foreign HTML sorts three ways. -- **7 — Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed - through ADF.** CommonMark spec suite runs against `markdownToHtml` from here. The README's - tagline and `package.json`'s `description` regain HTML (5g). -- **31 — Make the branch figure the coverage floor is read against repeatable.** Three Node test - legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%, and the - total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage` counts branches - off V8's own coverage, which the runner's parallel files and V8's optimization make run-dependent, - so the number the floor is read against is not the code's alone. The floor of 98 holds today on - 0.8 points of slack and `docs/decisions.md` §The coverage floors says it only ever moves upward, - so the first raise to the measured figure reddens a run that changed nothing. Make the - measurement repeatable, or state the number the floor may be raised to and why it is not the - measured one. -- **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on one - paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line - before riding the carry, quadratic in the runs (Goal 8), and the plain reduction's - `spellableLine` drops one mark per re-emit the same way. Make both linear. -- **42 — Trim a text leaf's trailing blanks in linear time.** `plain-inline.ts`'s `leafEdges` finds - the trail with an unanchored `/[ \t]*$/`, quadratic in a run of blanks inside one leaf: a - paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in `adfToPlainMarkdown` (Goal 8). Scan backward, - as the expand title's trim does. -- **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether - `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral - symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as - punctuation — and, where they do, read the code point, with a fixture per direction. -- **38 — Spell a lone surrogate in a text node so it survives a UTF-8 encode.** `adfToMarkdown` - emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values already escape - it. -- **5f — Publish the bundle size, after 7 changes it.** Measure the shipped artifact and put the - number in the README, kept honest by the release pipeline rather than by a human re-reading it. - The quantity is what a consumer downloads and loads: the tarball `npm pack` produces, its unpacked - `dist`, and the built JavaScript minified + gzipped — the figure the competitors advertise - (marklassian's "12kb") and the only apple-to-apple one, since ours ships tsc's unminified output - and no minifier yet (decide here whether to minify for the build or report the unminified gzip). A - publish/pipeline leg measures it and fails when the README figure drifts, so the number can't rot; - the figure lands in README §The package beside the "no runtime dependencies" claim. Measured - today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB. -- **5g — Reweight the README for the reader.** It opens with the pre-launch rationale — Atlassian's - REST APIs, `pf-editor-service/convert` being decommissioned, a link to JRACLOUD-77436 — where a - shipped package should answer what it is, what it does and for whom first, then the shortest - runnable example. The background goes entirely, no endpoint, ticket or - "why" note left. The top follows the package-README order: an npm version badge and the Gitea - Actions badge, a tagline that is also `package.json`'s `description`, a feature list and a - one-line table of contents, then install and the shortest runnable example; a table of everything - exported sits near the bottom. The README documents HTML as it documents markdown, the tagline and - `description` naming both, the lossy pair, and the flavours it writes and reads by name — GitHub - Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for - either finds the package. -- **44 — Cut `AGENTS.md` §7's empty-release bullet to what the maintainer's global working loop - leaves open.** The bullet restates that loop's rule for an empty release, in a sentence - `~/.claude/CLAUDE.md` → "Prose" bans; keep only its weighing by the personas and - `docs/decisions.md`, which the global loop does not hold. +`Score = -R - S/4 + 2*A + 2*G*W` -## 0.3.0 +`Bar = 9` -- **5e — Keep the release path publishing past npm's bypass-2FA token retirement.** `0.1.0` - published only once the npm token carried **Bypass 2FA**: the account requiring no 2FA on writes - was not enough, and npm answered `EOTP` until the token itself bypassed. npm retires bypass-2FA - tokens for direct publishing around January 2027, leaving them `npm stage publish`, which a - maintainer approves with 2FA; its replacement — trusted publishing over OIDC — supports - GitHub-hosted Actions, GitLab.com's shared runners and CircleCI's cloud, self-hosted runners - planned without a date. So the release path has an expiry date and no drop-in successor yet. - Revisit: whether npm has added Gitea or self-hosted OIDC, and otherwise whether the release moves - to the staged publish — which fits badly with publish-on-merge, and is the trade to weigh rather - than discover on a red release run. It stays out of `0.2.0` knowing the cutoff may land first. -- **9 — Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on - the library's browser build.** +`Next ID = 49` -## 0.4.0 +| Goal | W | +|---|---| +| 1 | 1.00 | +| 2 | 0.89 | +| 3 | 0.78 | +| 4 | 0.67 | +| 5 | 0.56 | +| 6 | 0.44 | +| 7 | 0.33 | +| 8 | 0.22 | +| 9 | 0.11 | -- **8 — Ship a CLI.** Its goal and its persona land in the README's `## Goals` and `## Audience` - with it. +## Items + +| ID | Release | Exempt | Item | R | S | A | G | Goals | Score | +|---|---|---|---|---|---|---|---|---|---| +| 40 | 0.2.0 | decision | **Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes.** | 6 | 7 | 8 | 9 | 1 | 26.2 | +| 7 | 0.2.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 3 | 25.8 | +| 45 | 0.2.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | +| 6 | 0.2.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 3 | 24.7 | +| 43 | 0.2.0 | | **Give each markdown input its own reader, strict to its own standard.** | 6 | 7 | 8 | 9 | 3, 4 | 22.3 | +| 38 | 0.2.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | +| 47 | 0.2.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 | +| 34 | 0.2.0 | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 3 | 3 | 6 | 3, 4 | 12.6 | +| 42 | 0.2.0 | | **Trim a text leaf's trailing blanks in linear time.** | 1 | 2 | 5 | 9 | 8 | 12.5 | +| 31 | 0.2.0 | | **Make the branch figure the coverage floor is read against repeatable.** | 2 | 3 | 3 | 4 | 1 | 11.2 | +| 33 | 0.2.0 | | **Make a carried mark run cost the line one re-emit.** | 4 | 5 | 6 | 9 | 8 | 10.7 | +| 46 | 0.2.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 7, 9 | 10.3 | +| 48 | 0.3.0 | question | **Keep the release path publishing past npm's bypass-2FA token retirement.** | 4 | 4 | 8 | 3 | 9 | 11.7 | +| 9 | 0.3.0 | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 2 | 6 | 6 | 8 | 9 | 10.3 | +| 8 | 0.4.0 | question | **Ship a CLI.** | 4 | 7 | 5 | 5 | 9 | 5.3 | + +## Details + +### 40. Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes. + +Today it holds for editor-normal documents only: two adjacent text nodes with the same marks merge, +an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines and bots +build. Spell each so it reads back as written, CommonMark's spelling kept wherever none occurs; the +spellings are part of the chunk. `docs/decisions.md` §Equality is editor-normal, `spec/flavour.md` +and `corpus/README.md` follow, and the tests drop `toEditorNormal`. + +### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF. + +Builds on item 6. The CommonMark spec suite runs against `markdownToHtml` from here. The README's tagline and +`package.json`'s `description` regain HTML (item 47). + +### 45. Replace `isAdfDocument` with a reader returning `Result`. + +Goal 1 has every call return a result; the boolean guard is the one export that does not, and it +cannot say which branch refused, where `not-an-adf-document`'s message already does. Breaking: +`MIGRATION.md` shows the guard's replacement. + +### 6. Specify the HTML dialect. + +Element-by-element mapping, the `data-*` fidelity scheme, the opaque-carry form, and the documented +foreign-element set `htmlToAdf` accepts — the set `markdownToAdf` shares (`spec/flavour.md` §Raw +HTML in input). The set sorts per `docs/decisions.md` §Foreign HTML sorts three ways. + +### 43. Give each markdown input its own reader, strict to its own standard. + +Today `markdownToAdf` reads CommonMark and the lossless flavour as one input: text shaped like a +directive, a pipe table or a `~~` pair becomes a flavour node where CommonMark reads plain text. A +caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour, +read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. + +### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. + +`adfToMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values +already escape it. + +### 47. Open the README with what the package is, what it does and for whom. + +It opens with the pre-launch rationale — Atlassian's REST APIs, `pf-editor-service/convert` being +decommissioned, a link to JRACLOUD-77436 — where a shipped package should answer what it is, what it +does and for whom first, then the shortest runnable example. The background goes entirely, no +endpoint, ticket or "why" note left. The top follows the package-README order: an npm version badge +and the Gitea Actions badge, a tagline that is also `package.json`'s `description`, a feature list +and a one-line table of contents, then install and the shortest runnable example; a table of +everything exported sits near the bottom. The README documents HTML as it documents markdown, the +tagline and `description` naming both, the lossy pair, and the flavours it writes and reads by name +— GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a +search for either finds the package. + +### 34. Read emphasis flanking by the whole character beside an astral symbol. + +Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an +astral symbol — a lone surrogate is neither punctuation nor symbol, where CommonMark reads `😀` as +punctuation — and, where they do, read the code point, with a fixture per direction. + +### 42. Trim a text leaf's trailing blanks in linear time. + +`plain-inline.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic in a run +of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in +`adfToPlainMarkdown`. Scan backward, as the expand title's trim does. + +### 31. Make the branch figure the coverage floor is read against repeatable. + +Three Node test legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and +96.23%, and the total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage` +counts branches off V8's own coverage, which the runner's parallel files and V8's optimization make +run-dependent, so the number the floor is read against is not the code's alone. The floor of 98 +holds today on 0.8 points of slack and `docs/decisions.md` §The coverage floors says it only ever +moves upward, so the first raise to the measured figure reddens a run that changed nothing. Make the +measurement repeatable, or state the number the floor may be raised to and why it is not the +measured one. + +### 33. Make a carried mark run cost the line one re-emit. + +`adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking +cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and the plain +reduction's `spellableLine` drops one mark per re-emit the same way. Make both linear. + +### 46. Publish the bundle size in the README, failing the release pipeline when it drifts. + +Lands after item 7, which changes it. The quantity is what a consumer downloads and loads: the +tarball `npm pack` produces, its unpacked `dist`, and the built JavaScript minified + gzipped — the +figure the competitors advertise (marklassian's "12kb") and the only apple-to-apple one, since ours +ships tsc's unminified output and no minifier yet (decide here whether to minify for the build or +report the unminified gzip). The figure lands in README §The package beside the "no runtime +dependencies" claim. Measured today, unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped +45.6 kB. + +### 48. Keep the release path publishing past npm's bypass-2FA token retirement. + +`0.1.0` published only once the npm token carried **Bypass 2FA**: the account requiring no 2FA on +writes was not enough, and npm answered `EOTP` until the token itself bypassed. npm retires +bypass-2FA tokens for direct publishing around January 2027, leaving them `npm stage publish`, which +a maintainer approves with 2FA; its replacement — trusted publishing over OIDC — supports +GitHub-hosted Actions, GitLab.com's shared runners and CircleCI's cloud, self-hosted runners planned +without a date. So the release path has an expiry date and no drop-in successor yet. Revisit: +whether npm has added Gitea or self-hosted OIDC, and otherwise whether the release moves to the +staged publish — which fits badly with publish-on-merge, and is the maintainer's trade to weigh +rather than discover on a red release run. It stays out of `0.2.0` knowing the cutoff may land +first. + +### 8. Ship a CLI. + +No goal or persona in the README covers it yet; the maintainer names both before it is built, and +they land in the README's `## Goals` and `## Audience` with it.