From fb972858997f628453c7fff6bff960cf094e9bfb Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 03:54:34 +0200 Subject: [PATCH 1/4] =?UTF-8?q?36b=20-=20=C2=A78,=20=C2=A79=20and=20=C2=A7?= =?UTF-8?q?14's=20decisions=20move=20to=20docs/decisions.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 103 +++----------------- README.md | 3 +- docs/decisions.md | 124 ++++++++++++++++++++++++ spec/flavour.md | 39 ++++---- src/conformance/commonmark-spec.test.ts | 2 +- src/markdown/parse/directive-nodes.ts | 2 +- src/markdown/parse/inline-content.ts | 2 +- src/result.test.ts | 2 +- todo.md | 2 - 9 files changed, 161 insertions(+), 118 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9a92350..7c0b9d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,96 +28,24 @@ In `docs/decisions.md`: - 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 +- No streaming APIs +- No performance budget ## 7. Nothing about any consumer No Jira client, no HTTP, no REST shapes, no issue keys, no actual consumer named anywhere. Design against the README's personas. -## 8. Semver: the formats are API - -The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing -differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR. -Pre-1.0, normal 0.x rules. A spelled node's content model is part of that contract — leaf or -container is the model, not the syntax — so giving a spelled node's model content it had not, or -taking it away, is MAJOR whatever ADF's own schema does. - -The error surface is a contract too; `README.md` §The errors states it to the consumer, and the -types in `src/result.ts` hold its shape. - -### The code list - -- Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose - name reads true of it in both directions; where none does and a plain name exists, a new code — - in any 0.x minor, and after 1.0 only in a MAJOR (the maintainer, 2026-09-18). -- A refusal whose cause is this library's own invariant rather than the input takes the existing - code nearest what the consumer sees — a document that does not convert is - `unsupported-node-shape` — since a code no input reaches is one no consumer can switch on (the - maintainer, 2026-09-20). -- 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 (`unspellable-link`, the - maintainer, 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides - the carry with its node. - -### Which code a cause takes - -- 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-character` the text node and the code block 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 `!adf: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. What the grammar itself refuses stays a - claim code, key order among it, and a leaf given a body is refused at its opener, as a container - missing its closer is (the maintainer, 2026-09-16). -- 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:carry`, whose carry is the fence — and a well-formed form in the wrong place - is `unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one - type. -- 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 of a shape - 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. -- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document` - emitting — no document holds one, so no round-trip crosses them. - -### `message` and `path` - -- 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: `\!adf:` for a directive, block line and - inline alike, `\|` for every pipe row. -- `not-an-adf-document` carries the document's own path throughout: seven of the guard's eight - branches read the document's own shape, and threading a path to the eighth — 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. - ## 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 bump commit renames `CHANGELOG.md`'s `## Unreleased` to the version. -- Docs on `main` describe the release being built rather than the version npm holds, so they match - it the moment the bump publishes; add no interim note marking the gap (the maintainer, - 2026-09-16). -- 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. - Exact versions: `save-exact=true` in `.npmrc`. - 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 @@ -316,16 +244,6 @@ Applies everywhere: comments, every markdown file in this repo (this one include One-line commit messages and PR titles; short PR summaries. No AI-attribution markers, ever. -## 14. Non-goals - -No network or filesystem I/O, no name→id resolution (`docs/decisions.md` §Names stay text), no ADF -schema validation or exported validator — a refusal that keeps the round-trip is not schema -validation, so the one a spelled node carrying the same mark type twice earns stays, and input -nesting a spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS -(`docs/decisions.md` §The HTML dialect), no streaming APIs, no performance budget past §11's -scanning rule — nothing here is tuned, and no figure is promised. A CLI is a later goal (`todo.md`), -not a non-goal. - ## 15. The working loop `todo.md` lists what is left under the release that ships it, in shipping order. One item per @@ -347,7 +265,8 @@ Per chunk: reworded for them into `CHANGELOG.md`'s `## Unreleased` — report, stop. Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a -bump on `main` publishes, §9 — every release is the maintainer's) and the `NPM_TOKEN` secret. +bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the +maintainer's) and the `NPM_TOKEN` secret. ### Ask, don't guess diff --git a/README.md b/README.md index 538deea..10b174c 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,8 @@ emit refuses: - Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist 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. -- The emitted formats are semver surface (AGENTS.md §8). +- The emitted formats are semver surface + ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)). - **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides `data-*` attributes. Foreign HTML maps a documented element set, which markdown's raw HTML reads through as well, and a construct outside it is an error; well-formed HTML only — no tag-soup diff --git a/docs/decisions.md b/docs/decisions.md index b0c84c4..d631f78 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -182,3 +182,127 @@ an npm consumer. Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public, LICENSE in place, before the first publish. + +## The formats are API + +2026-08-23, content models 2026-09-16, the maintainer. Goals 1 and 6. Valid while consumers store +what the library emits. + +The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing +differently, or not at all, is MAJOR; new syntax while old output still round-trips is MINOR. +Pre-1.0, normal 0.x rules. A spelled node's content model is part of that contract — leaf or +container is the model, not the syntax — so giving a spelled node's model content it had not, or +taking it away, is MAJOR whatever ADF's own schema does. + +The error surface is a contract too; `README.md` §The errors states it to the consumer, and the +types in `src/result.ts` hold its shape. + +## The code list + +2026-08-25, the maintainer; dated below where a rule came later. Goal 6. Valid while a consumer +switches on `code` with no `default`. + +- Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose + name reads true of it in both directions; where none does and a plain name exists, a new code — + in any 0.x minor, and after 1.0 only in a MAJOR (2026-09-18). +- A refusal whose cause is this library's own invariant rather than the input takes the existing + code nearest what the consumer sees — a document that does not convert is + `unsupported-node-shape` — since a code no input reaches is one no consumer can switch on + (2026-09-20). +- 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 (`unspellable-link`, + 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides the carry + with its node. + +## Which code a cause takes + +2026-08-28, the maintainer; dated below where a rule came later. Goal 6. Valid while a consumer +handles one cause alike whichever node, attribute or direction raised it. + +- 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-character` the text node and the code block 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 `!adf: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. What the grammar itself refuses stays a + claim code, key order among it, and a leaf given a body is refused at its opener, as a container + missing its closer is (2026-09-16). +- 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:carry`, whose carry is the fence — and a well-formed form in the wrong place + is `unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one + type (2026-09-01). +- 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 of a shape + 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 (2026-09-23). +- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document` + emitting — no document holds one, so no round-trip crosses them (2026-09-23). + +## `message` and `path` + +2026-09-03, the path 2026-09-23, the maintainer. Goals 3 and 6. Valid while a person fixing the +input reads `message`. + +- A message names the violation, not the rule alone — a rule by itself states a truth the reader + 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: `\!adf:` for a directive, block line and + inline alike, `\|` for every pipe row. +- `not-an-adf-document` carries the document's own path throughout: seven of the guard's eight + branches read the document's own shape, and threading a path to the eighth — a malformed node + anywhere in the tree — wants the manual stack the no-recursion rule (`AGENTS.md` §11) forces, + whose empty half no input reaches. The message names the violation instead. + +## Publish on a version bump + +2026-08-23, converging 2026-09-03, the maintainer. Goal 7. Valid while CI on `main` holds the npm +token. + +`package.json` version on `main` is the source of truth. CI on `main`: tests green and 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. + +## Docs describe the release being built + +2026-09-16, the maintainer. Goal 7. Valid while a bump on `main` publishes. + +Docs on `main` describe the release being built rather than the version npm holds, so they match it +the moment the bump publishes; add no interim note marking the gap. + +## No schema validation + +2026-08-23, the mark refusal 2026-08-25, the maintainer. Goal 1. Valid while the site a document +is saved to validates it. + +No ADF schema validation or exported validator. A refusal that keeps the round-trip is not schema +validation, so the one a spelled node carrying the same mark type twice earns stays, and input +nesting a spelling inside its own kind (`*(*a*)*`) names that mark once. + +## No streaming APIs + +2026-08-23, the maintainer. Goal 8. Valid while a document fits in memory. + +A call takes a whole document and returns a whole result. + +## No performance budget + +2026-08-23, the maintainer. Goal 8. Valid while no persona needs a speed figure. + +Nothing is tuned past the scanning rule (`AGENTS.md` §11), and no figure is promised. diff --git a/spec/flavour.md b/spec/flavour.md index f1ed888..f72f214 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -1,12 +1,12 @@ # The markdown flavour The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain -CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that -matches directive syntax below or reads as a pipe table is claimed by the flavour, and a matched -`~~` pair spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal) — and one gap: a -CommonMark image fits only as its own -title-less paragraph — mid-text and titled images are named errors. The emitted form is contract -(AGENTS.md §8). Per-node syntaxes build on this grammar in the sections below. +CommonMark is a subset apart from raw HTML (below), with three carve-outs: literal text that matches +directive syntax below or reads as a pipe table is claimed by the flavour, and a matched `~~` pair +spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal) — and one gap: a CommonMark +image fits only as its own title-less paragraph — mid-text and titled images are named errors. The +emitted form is contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this +grammar in the sections below. ## Canonical form @@ -76,13 +76,14 @@ normalizes to it through the round-trip. One grammar for everything CommonMark lacks, namespaced: every directive opens with the literal `!adf:`. A directive name is `[a-z][A-Za-z0-9]*` — the ADF node and mark names the sections below spell as directives. Recognition is syntactic and name-set-independent: anything matching the forms -below parses as a directive regardless of whether the name is known, and an unknown name is an -error result naming it at the opener, whatever follows it — so output an old emitter escaped stays -escaped, and erroring input gaining meaning later is MINOR, never a reparse (§8). Each name belongs -to one position, and a name the other one spells — a mark or an inline node written as a block -directive, a block node written inline — is a different error, naming the spelling it takes. Two -reserved names read back to no node: `carry` for the opaque carry, as both directive name and fence -info string, and `listBreak` for the leaf that parts two adjacent lists (Canonical form). +below parses as a directive regardless of whether the name is known, and an unknown name is an error +result naming it at the opener, whatever follows it — so output an old emitter escaped stays +escaped, and erroring input gaining meaning later is MINOR, never a reparse (`docs/decisions.md` +§The formats are API). Each name belongs to one position, and a name the other one spells — a mark +or an inline node written as a block directive, a block node written inline — is a different error, +naming the spelling it takes. Two reserved names read back to no node: `carry` for the opaque carry, +as both directive name and fence info string, and `listBreak` for the leaf that parts two adjacent +lists (Canonical form). **Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name` closes a container, and a name picks by what follows it in turn — a space or the line's end a block @@ -123,7 +124,7 @@ Which of the two a node takes is its content model, never the spelling: a model written as an opener–closer pair and one taking none as a leaf, so a leaf given a body and a container missing its closer are each a named error. A node holding no content whose model takes some is an empty pair. A spelled node's content model is contract in consequence — changing one is -MAJOR (AGENTS.md §8). +MAJOR (`docs/decisions.md` §The formats are API). Canonical spacing is the only spacing input reads: one space parts the name, `arg` and `{attrs}`, and one parts each attribute pair, with no padding inside the braces. Trailing whitespace on a @@ -184,11 +185,11 @@ HTML. ## Block nodes -The directive name is always the ADF node type. A container's body is the node's `content`; a -leaf has none. Every directive parses in any position — `markdownToAdf` builds exactly what is -written; validity against ADF's content models stays the author's business (AGENTS.md §14). It -parses only in the form the emitter picks, though: a directive spelling a node the emitter would -have written as CommonMark is a named error. +The directive name is always the ADF node type. A container's body is the node's `content`; a leaf +has none. Every directive parses in any position — `markdownToAdf` builds exactly what is written; +validity against ADF's content models stays the author's business (`docs/decisions.md` §No schema +validation). It parses only in the form the emitter picks, though: a directive spelling a node the +emitter would have written as CommonMark is a named error. Each section lists attributes as `name (type)`. A parenthesized value set documents what real payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by diff --git a/src/conformance/commonmark-spec.test.ts b/src/conformance/commonmark-spec.test.ts index 6c6bb85..d9dde4c 100644 --- a/src/conformance/commonmark-spec.test.ts +++ b/src/conformance/commonmark-spec.test.ts @@ -91,7 +91,7 @@ test('the refusal list is unique per example and names real examples', () => { for (const example of exampleToRefusal.keys()) assert.ok(spec.some((entry) => entry.example === example), `refusal ${example} names no example in the suite`) }) -// A mark is counted once per text node it touches (AGENTS.md §14). +// A mark is counted once per text node it touches (docs/decisions.md §No schema validation). const countKeys = ['a', 'blockquote', 'br', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'li', 'ol', 'pre', 'strong', 'ul'] const nodeElement: Record = { blockquote: 'blockquote', diff --git a/src/markdown/parse/directive-nodes.ts b/src/markdown/parse/directive-nodes.ts index a72ecd4..7235b69 100644 --- a/src/markdown/parse/directive-nodes.ts +++ b/src/markdown/parse/directive-nodes.ts @@ -69,7 +69,7 @@ export function readInlineDirectiveNode( return success(namedNode(name, attrs.value, undefined)) } -// A name the other position spells names that spelling, never the code a later MINOR may fill (AGENTS.md §8). +// A name the other position spells names that spelling, never the code a later MINOR may fill (docs/decisions.md §Which code a cause takes). function inlineSpellingFault(name: string): ConvertFault | undefined { const mark = inlineMarkSpellingFault(name) if (mark !== undefined) return mark diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 494ecc3..3017c03 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -477,7 +477,7 @@ function markType(character: string, used: number): string { return used === 2 ? 'strong' : 'em' } -// A node cannot carry one mark type twice (AGENTS.md §14). +// A node cannot carry one mark type twice (docs/decisions.md §No schema validation). function applyMark(nodes: readonly AdfNode[], mark: AdfMark): AdfNode[] { return nodes.map((node) => { const marks = nodeMarks(node) diff --git a/src/result.test.ts b/src/result.test.ts index d672ad0..a85fb23 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -25,7 +25,7 @@ function calledCodes(): string[] { return [...called].sort() } -// The list is frozen at 0.1.0 (AGENTS.md §8), so a code outliving its cause is a removal that costs a MAJOR. +// The list is frozen at 0.1.0 (docs/decisions.md §The code list), so a code outliving its cause is a removal that costs a MAJOR. test('every ConvertErrorCode is the code of a production call site, and every call site names a declared one', () => { assert.deepEqual(calledCodes(), declaredCodes()) }) diff --git a/todo.md b/todo.md index 305389f..778f9c8 100644 --- a/todo.md +++ b/todo.md @@ -7,8 +7,6 @@ one no goal serves is proposed as a goal and asked. Sources: `AGENTS.md`'s body, the settled text in this file's items, and `todo-history.md`, deleted with the bare `(28)` citations into it once nothing cites it. Split by `AGENTS.md` section where one chunk is too big. - - **36b — Move §8, §9 and §14's decisions.** The code list's rules, release automation and the - non-goals. - **36c — Move §10 and §11's decisions.** The engine legs, floors, size ratchet, bounds, spellings and layout; the style rules stay working rules. - **36d — Move the settled text in `todo.md`'s items and §15's dated rules, and point §15's -- 2.52.0 From e5771bc60bef84e6dd4cfd2dd0a104004b6798e7 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 03:55:29 +0200 Subject: [PATCH 2/4] 36b - review: no stale private flag, no undefined freeze --- docs/decisions.md | 6 ++---- src/result.test.ts | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index d631f78..3af8f1b 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -210,8 +210,7 @@ switches on `code` with no `default`. `unsupported-node-shape` — since a code no input reaches is one no consumer can switch on (2026-09-20). - 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 (`unspellable-link`, - 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides the carry + the spelling and the code goes (`unspellable-link`, 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides the carry with its node. ## Which code a cause takes @@ -268,8 +267,7 @@ token. `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. +deliberate semver judgment. `publish.sh` is that job. 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 diff --git a/src/result.test.ts b/src/result.test.ts index a85fb23..8503d13 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -25,7 +25,7 @@ function calledCodes(): string[] { return [...called].sort() } -// The list is frozen at 0.1.0 (docs/decisions.md §The code list), so a code outliving its cause is a removal that costs a MAJOR. +// Removing a code is breaking (docs/decisions.md §The code list), so a code must not outlive its cause. test('every ConvertErrorCode is the code of a production call site, and every call site names a declared one', () => { assert.deepEqual(calledCodes(), declaredCodes()) }) -- 2.52.0 From 50f97513e8ccbb5e9e8363051dcc46b01ab03398 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 03:56:16 +0200 Subject: [PATCH 3/4] 36b - review: publish entry matches publish.sh --- docs/decisions.md | 13 ++++++------- publish.sh | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 3af8f1b..8321bd6 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -210,8 +210,8 @@ switches on `code` with no `default`. `unsupported-node-shape` — since a code no input reaches is one no consumer can switch on (2026-09-20). - 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 (`unspellable-link`, 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides the carry - with its node. + the spelling and the code goes (`unspellable-link`, 2026-09-13). A cause the carry answers gets + no code: a mark no spelling writes rides the carry with its node. ## Which code a cause takes @@ -265,12 +265,11 @@ input reads `message`. 2026-08-23, converging 2026-09-03, the maintainer. Goal 7. Valid while CI on `main` holds the npm token. -`package.json` version on `main` is the source of truth. CI on `main`: tests green and 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. +`package.json` version on `main` is the source of truth. CI on `main`: tests green and the version +not yet on npm → publish and tag `vX.Y.Z`. No bump, no deploy. `publish.sh` is that job. -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 +The publish and the tag each check their own end state — the version on npm, the tag on the +remote — 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 diff --git a/publish.sh b/publish.sh index 9f4ec89..352d13b 100755 --- a/publish.sh +++ b/publish.sh @@ -30,7 +30,6 @@ fi published=$(leg "ask npmjs for $name@$version ($node_image)" published_version "$name" "$version") tagged=$(leg "ask origin for v$version" git ls-remote --tags origin "v$version") -# Both steps observe their own end state, so a partial run converges on the next push to main. if [ -z "$published" ]; then : "${NPM_TOKEN:?the publish needs NPM_TOKEN}" leg "install ($node_image)" in_image "$node_image" npm ci -- 2.52.0 From d2a3d74030be24d1f1bc948c0784b6e6e65d0f5d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 10:22:08 +0200 Subject: [PATCH 4/4] 36b - Goals 8 and 9: correct before fast, fast once correct --- AGENTS.md | 2 -- README.md | 4 ++++ docs/decisions.md | 12 ------------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7c0b9d0..ae7e0dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,8 +35,6 @@ In `docs/decisions.md`: - Publish on a version bump - Docs describe the release being built - No schema validation -- No streaming APIs -- No performance budget ## 7. Nothing about any consumer diff --git a/README.md b/README.md index 10b174c..9134497 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ In priority order. the emitted formats are. 7. **Nothing in the way.** No runtime dependencies, no I/O, no configuration, no host API: ESM on any ES2022 engine, in a browser as readily as on a server, installed from public npm. +8. **Correct before fast.** A whole document in, a whole result out, one call; no input makes a + call hang or overflow the stack. +9. **Fast once correct.** Conversion time grows linearly with the document wherever the goals above + allow it; a faster path that risks one of them is not taken. ## Audience diff --git a/docs/decisions.md b/docs/decisions.md index 8321bd6..b5c3f33 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -291,15 +291,3 @@ is saved to validates it. No ADF schema validation or exported validator. A refusal that keeps the round-trip is not schema validation, so the one a spelled node carrying the same mark type twice earns stays, and input nesting a spelling inside its own kind (`*(*a*)*`) names that mark once. - -## No streaming APIs - -2026-08-23, the maintainer. Goal 8. Valid while a document fits in memory. - -A call takes a whole document and returns a whole result. - -## No performance budget - -2026-08-23, the maintainer. Goal 8. Valid while no persona needs a speed figure. - -Nothing is tuned past the scanning rule (`AGENTS.md` §11), and no figure is promised. -- 2.52.0