Skip to content

fix: bump carve-grammars onto the build that parses {:TAG} and keeps a quote's attribution - #14

Merged
dereuromark merged 2 commits into
mainfrom
chore/bump-carve-grammars-6436a4ec
Aug 14, 2026
Merged

fix: bump carve-grammars onto the build that parses {:TAG} and keeps a quote's attribution#14
dereuromark merged 2 commits into
mainfrom
chore/bump-carve-grammars-6436a4ec

Conversation

@dereuromark

Copy link
Copy Markdown
Contributor

markup-carve/carve-wysiwyg#12 is closed, but the symptom it reported still
happens at the pinned grammar build. Typing

A [bonjour]{:fr} end.

in the source pane produces no span, and the pane writes back

A \[bonjour]{:fr} end.

The mark is missing because the editor's loader parses with the engine
carve-grammars installs for ITSELF - nested under carve-grammars, where no pin
in this repository can reach it - and the pinned build carries the old one. The
escape follows from the missing mark: an unrecognized construct is one text
node, and the serializer escapes the bracket that would otherwise open a link on
the way back out.

The pin moves 639db73 -> c0e7291. Both are merged commits on carve-grammars
main; c0e7291 is the merge of markup-carve/carve-grammars#218.

Read the installed tree, not the version string

The version string says 0.1.4 at both pins, so it proves nothing. What the
installed files carry:

node_modules/@markup-carve/carve-grammars 639db73 c0e7291
lang in tiptap/extensions/carve-span.js 4 occurrences, with parseHTML 4 occurrences, with parseHTML
the ':' + value sugar in tiptap/serializer.js present, line 1296 present, line 1343
attribution in tiptap/carve-to-pm.js 0 occurrences 4 occurrences
the engine it nests 25e2c58 8d38a03

The span half was already in the grammar at the old pin. The engine underneath
it was not, which is exactly why the earlier fix never reached a user, and it is
why the check has to read carve-to-pm.js and the nested engine rather than the
package version.

Both halves, end to end through the editor

Measured at three grammar pins, through the app's own import and serialize path
rather than the loader alone:

639db73 (current) 75738b3 (the engine bump) c0e7291 (this PR)
A [bonjour]{:fr} end. carries a carveSpan with lang no mark at all fr fr
the same source round-trips A \[bonjour]{:fr} end. A [bonjour]{:fr} end. A [bonjour]{:fr} end.
a quote with ^ Steve Jobs, edited elsewhere, serialized keeps it LOSES it keeps it
npm test 1 failed, 18 passed 3 failed, 16 passed 19 passed

The middle column is the point of the sequencing. The engine bump alone would
have traded one symptom for another: the attribution reached the editor as a
figure caption before it, is the quote's attribution field after it, and the
projection for that field is what markup-carve/carve-grammars#218 added. Pinning
this repo to 75738b3 would have shipped a document-losing editor.

The language-attribute assertion is on the carveSpan mark and the absent
backslash, not on the round-tripped string: an unrecognized [bonjour]{:fr} is
one text node the serializer writes back verbatim, so the string is a fixed
point at both pins and a string assertion would be vacuous.

The attribution assertions EDIT before serializing. Loading and serializing an
untouched document returns the whole-document source envelope verbatim and
passes at any pin; the envelope is keyed to a fingerprint of the untouched
document, so the first edit is what discards it. A load-only check cannot see
this bug.

Proof the tests are reached

Reverting the pin to 639db73 with the tests in place:

× parses the {:fr} shorthand typed in the source pane onto the span mark
  Tests  1 failed | 18 passed (19)

Pinning 75738b3, the engine bump without the projection:

× keeps the attribution when something else is edited
× holds the attribution in an editable node, not only the source envelope
× keeps an attribution written one blank line below the quote
  Tests  3 failed | 16 passed (19)

And the controls fail on an expectation the build cannot produce, so they are
not passing by never being reached:

× parses the {:fr} shorthand typed in the source pane onto the span mark
  AssertionError: expected 'fr' to be 'MUTANT'
× keeps the attribution when something else is edited
  expect(out).toContain('^ MUTANT');
  Tests  2 failed | 17 passed (19)

The pin watchdog

scripts/check-carve-pins.mjs went red on the bump:

::error::@markup-carve/carve-grammars is pinned to a build written against spec
988fdc8ea42b, which is 31 commit(s) behind the spec cca18744f8fe that the
installed @markup-carve/carve build was written against.

True, and not this repository's to act on. carve-grammars pins the engine its
own loader calls to an exact commit, and the git build in this lockfile IS that
commit, hoisted out of carve-grammars because this repo asks for a version
range. The only lever here is the grammar pin, and rolling it back to close the
gap gives up every fix that landed after it.

The gap also cannot bite in this direction: the app hands carve-grammars SOURCE,
not an AST. carveToProseMirror(source) parses with the engine carve-grammars
nests, and this repo's own engine is used only for the preview HTML in
carveToHtmlRaw, which never reaches the grammar.

So that arm now decides from the INSTALLED revisions rather than from how
package.json spells the dependency: when the lockfile's engine is the exact
commit the pinned grammar declares for itself, the finding is a warning naming
carve-grammars. Any other engine is one this repo installed, and pairing it with
an older grammar still fails. Measured against the reworked script:

probe verdict exit
this PR's pins warning, named at carve-grammars 0
pin names a commit not on carve-grammars main error, "Pin a merged commit" 1
lockfile disagrees with package.json error, names both commits 1
grammar behind, installed engine is not the one it bundles error, unchanged wording 1
pin falls behind main the existing warning 0

The stale-pin case is a warning rather than a failure, which is unchanged and
deliberate: the daily engine-drift job is where it surfaces, and failing every
run once main moves ahead by one commit would bury it.

Not in this PR

No version field is touched. The engine range ^0.1.2 is unchanged. The spec
revision gap the watchdog now warns about is carve-grammars' to close.

#12 reported that typing

```
A [bonjour]{:fr} end.
```

in the source pane produced no span, and the pane wrote back

```
A \[bonjour]{:fr} end.
```

That is still the behavior at the pinned grammar build. The mark is missing
because the loader parses with the engine carve-grammars installs for ITSELF -
nested under carve-grammars, where no pin in this repository can reach it - and
the pinned build carries the old one. The escape follows from the missing mark:
an unrecognized construct is one text node, and the serializer escapes the
bracket that would otherwise start a link on the way back out.

The pin moves from 639db73 to c0e7291, both merged commits on carve-grammars
main. Verified by reading the installed tree rather than the version string,
which says 0.1.4 at both pins and proves nothing:

| `node_modules/@markup-carve/carve-grammars` | 639db73 | c0e7291 |
| --- | --- | --- |
| `lang` in `tiptap/extensions/carve-span.js` | 4 occurrences, with `parseHTML` | 4 occurrences, with `parseHTML` |
| the `':' + value` sugar in `tiptap/serializer.js` | present | present |
| `attribution` in `tiptap/carve-to-pm.js` | 0 occurrences | 4 occurrences |
| the engine it nests | 25e2c58 | 8d38a03 |

The span half was already in the grammar at the old pin; the engine underneath
it was not, which is the whole reason the earlier fix did not reach a user.

Two tests cover the halves through the app's own import and serialize path.
The language attribute one asserts on the `carveSpan` mark and on the absent
backslash rather than on the round-tripped string: an unrecognized
`[bonjour]{:fr}` writes back verbatim, so the string is a fixed point at both
pins. The attribution one edits before serializing, because loading and
serializing an untouched document returns the source envelope verbatim and
passes at any pin.
Moving the grammar pin turned the spec-freshness arm red:

```
::error::@markup-carve/carve-grammars is pinned to a build written against spec
988fdc8ea42b, which is 31 commit(s) behind the spec cca18744f8fe that the
installed @markup-carve/carve build was written against.
```

The reading is true, and it is not this repository's to act on. carve-grammars
pins the engine its own loader calls to an exact commit; the git build in this
lockfile IS that commit, hoisted out of carve-grammars because this repo asks
for a version range. The only lever here is the grammar pin, and rolling it back
to close the gap would give up every fix that landed after it, including the one
this bump exists for.

Nor can the gap bite this way round: the app hands carve-grammars SOURCE, not an
AST. `carveToProseMirror(source)` parses with the engine carve-grammars nests,
and this repo's own engine is used only for the preview HTML in
`carveToHtmlRaw`, which never reaches the grammar. There is no path where an
older grammar is handed a newer engine's tree.

So the arm now decides from the INSTALLED revisions rather than from how
package.json spells the dependency: when the lockfile's engine is the exact
commit the pinned grammar declares for itself, the finding is a warning naming
carve-grammars as the repository that can close it. Any other engine is one this
repo installed, and pairing it with an older grammar still fails.

Nothing else is relaxed. Measured against the reworked script:

- a pin naming a commit that is not on carve-grammars main: error, exit 1
- a lockfile that disagrees with package.json: error, exit 1
- a grammar pin whose own engine is not the installed one, with the grammar
  behind: error, exit 1
- the pin falling behind main: the existing warning, unchanged
@dereuromark
dereuromark merged commit a18466e into main Aug 14, 2026
3 checks passed
@dereuromark
dereuromark deleted the chore/bump-carve-grammars-6436a4ec branch August 14, 2026 18:42
@dereuromark

Copy link
Copy Markdown
Contributor Author

Heads up: half of what this pin brought in is being withdrawn. markup-carve/carve#1213 reverts PART 9 section 4a, so a caption on a block quote is a figure caption again and block_quote.attribution stops existing - the HTML Standard requires a quotation's attribution OUTSIDE the blockquote, and figure was already the generic captioned wrapper, so the clause had no leg to stand on. Nothing shipped, so there is no migration.

The {:TAG} half of this pin is unaffected and stays.

Tracked in markup-carve/carve-grammars#219: the projection reverts there first, then this pin moves onto the grammars release that carries it. No action needed here until then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant