feat(book): shared counters across proof:* kinds (#34)#40
Conversation
Adds `numbering.<proof-kind>.counter: <other-kind>` aliasing — LaTeX
`\newtheorem{lemma}[theorem]{Lemma}` parity. When set, the aliased kind
steps the slot owner's counter so interleaved theorem/lemma/proposition
sequences render with the same enumerators as the LaTeX PDF (closes the
residual gap left after #28's `scope: section`).
Slot-owner-wins for counter mechanics (`start`, `format`, `continue`,
`reset_on_part`, `scope`) — validator warns and drops those fields on
aliased kinds. `label` and `template` stay per-kind so rendered text
still reads "Lemma 1.2.2" while sharing the theorem slot.
Engine builds a precomputed resolved-kind map at ReferenceState
construction; both `targetCounts[]` and `lastScopeKeyByKind[]` route
through it. Family check (proof-family only) and cycle detection
emit warnings via fileWarn; cross-family targets and cycles degrade
gracefully to per-kind behavior. Transitive resolution (`a→b→c`)
flattens at map-build time so the hot path stays free of recursion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds support for shared counters across proof:* kinds in book-mode numbering, enabling LaTeX-style \newtheorem{lemma}[theorem]{Lemma} behavior so interleaved theorem-family environments increment a single shared counter slot while keeping per-kind rendering.
Changes:
- Introduces
numbering.<proof-kind>.counterin frontmatter types + validation, including normalization of bare proof targets (e.g.theorem→proof:theorem) and warnings/dropping of slot-owner-only fields on aliased kinds. - Implements resolved counter-slot mapping in
ReferenceState.incrementCount(including transitive resolution + cycle detection) so counter mechanics and scope resets use the slot owner. - Adds extensive unit tests covering shared-slot sequencing, scope reset behavior, transitive resolution, cycle handling, and cross-family alias rejection.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/myst-transforms/src/enumerate.ts | Builds and applies a resolved counter-slot map so aliased proof kinds share the same counter + scope-reset slot. |
| packages/myst-transforms/src/enumerate.spec.ts | Adds test coverage for shared proof counters (including book-dp1 table cases, scope resets, cycles, and cross-family rejection). |
| packages/myst-frontmatter/src/numbering/validators.ts | Parses counter, normalizes bare proof-family targets, and drops slot-owner-only fields on aliased entries. |
| packages/myst-frontmatter/src/numbering/types.ts | Documents and types the new NumberingItem.counter option. |
| packages/myst-frontmatter/src/numbering/numbering.spec.ts | Adds validator tests for counter normalization and owner-field dropping rules. |
| packages/myst-common/src/rule-severities.ts | Updates an inline comment about warning counts for validPageFrontmatter. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Object.entries(output) | ||
| .filter(([key]) => !NUMBERING_OPTIONS.includes(key)) | ||
| .forEach(([key, item]) => { | ||
| if (!item?.counter) return; | ||
| const normalized = normalizeCounterTarget(key, item.counter); | ||
| if (normalized !== item.counter) item.counter = normalized; | ||
| const itemOpts = incrementOptions(key, opts); | ||
| for (const field of COUNTER_OWNER_FIELDS) { | ||
| if (defined((item as any)[field])) { | ||
| validationWarning( | ||
| `'${field}' is read from the slot owner ('${item.counter}') when 'counter' is set; ignoring on '${key}'`, | ||
| itemOpts, | ||
| ); | ||
| delete (item as any)[field]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Good catch — fixed in 4728068. Gated the owner-field drop on isProofFamilyKindForCounter(key) so cross-family entries (e.g. figure.counter: proof:theorem) now keep their counter field (engine still emits the family warning) and all their owner-fields intact. Added a regression test in numbering.spec.ts pinning the figure.counter: proof:theorem, start: 5, scope: section case.
| * Slot-owner-wins for counter mechanics: `start`, `format`, `continue`, | ||
| * `reset_on_part`, and `scope` are read from the slot owner. Setting | ||
| * those on an aliased kind emits a validator warning and is dropped. | ||
| * Cross-family aliasing and cycles are detected at config-load time | ||
| * and degrade to per-kind behaviour with a warning. | ||
| */ |
There was a problem hiding this comment.
Fair — the docstring was mine and it was wrong. Aligned in 4728068: now says cross-family + cycle detection happens at transform time (in myst-transforms/src/enumerate.ts when the resolved-counter map is built), and notes that owner-fields on cross-family entries are left intact since the alias is ignored anyway.
A user typo like `figure.counter: proof:theorem` is a cross-family alias the engine refuses to honor at runtime. Previously the validator still dropped `figure.start`/`figure.scope` etc. because the owner-field-override loop ran for any kind with `counter` set, silently deleting valid configuration. Gate the drop on `isProofFamilyKindForCounter(key)` so cross-family entries keep their `counter` field (engine emits the family warning) and their owner-fields intact. Also align the `NumberingItem.counter` docstring with actual behavior: cross-family + cycle detection happens at transform time, not config-load time. Found by Copilot review on PR #40. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI lint:format flagged this — the two-arg signature fit on one line under prettier's printWidth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Smoke test — book-dp1 end-to-end ✅Cloned book-dp1 (mystmd-conversion branch) to a fresh Enumerators on the proof nodes match the LaTeX PDF verbatim:
Resolved cross-references ( No regressions: Ready to merge. |
Records PR #40 (#34 — LaTeX `\newtheorem{name}[other]` counter sharing) as id 6 with squash SHA a63d53f and tag qe-v6, and adds it to the book-mode-with-section-scope upstream candidate alongside #22, #28, and #33 — they share the same auto-prefix machinery and form one coherent upstream story. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #34.
Summary
Adds
numbering.<proof-kind>.counter: <other-kind>— LaTeX\newtheorem{lemma}[theorem]{Lemma}parity. The aliased kind steps the slot owner's counter, so interleaved theorem/lemma/proposition sequences render with the same enumerators as the LaTeX PDF.t-nsll-rsnbt-bfptl-rxrnDesign — Tier 1 only
Per the spec in #34 (and the review at #34#issuecomment-4514310125):
start,format,continue,reset_on_part,scopeare read from the slot owner. Validator warns and drops those fields when set on an aliased kind.label,template, and the enumerator wrap are unchanged so an aliasedlemmastill reads "Lemma 1.2.2" while sharing the theorem counter slot.a→b→cflatten toa→c, b→c. No recursion in the hot path.counter: theorem) within the proof family normalize to the fully-qualified form (proof:theorem).enumerate.ts(parallel toheadingFormats) — keeps the validator pure andNumberingItemclean.Tier 2 (
shared_counters:list sugar) deferred — see issue body and the spec-tightening comment.Where it lives
packages/myst-frontmatter/src/numbering/types.ts— addscounter?: stringtoNumberingItem.packages/myst-frontmatter/src/numbering/validators.ts— addscounterparsing, normalizes bare proof-family targets, warns + drops owner-fields (start,format,continue,reset_on_part,scope) on aliased kinds.packages/myst-transforms/src/enumerate.ts— newbuildResolvedCounterMap+effectiveCounterKindhelpers;ReferenceStatebuilds the map once at construction;incrementCountroutestargetCounts[]ANDlastScopeKeyByKind[]through the resolved kind (one shared slot + one shared scope-key entry, so scope crossings reset exactly once).Frames the engine change as generalizing the existing hardcoded
const countKind = kind === TargetKind.subequation ? TargetKind.equation : kind;pattern — same shape, just config-driven.Test plan
enumerate.spec.ts, 6 innumbering.spec.ts)t-nsl→1.2.1…t-hartgrob→2.1.3)enumeratorwrap (rendering stays per-kind)a→b→cflattens correctlya→b→awarns + falls back to per-kindnpm testgreen (73/73 task suites)Sequencing: lands as qe-v6 on top of qe-v5 (#33). Builds on #28's
scope: section. Purely additive to fork-only code.🤖 Generated with Claude Code