Skip to content

feat(book): shared counters across proof:* kinds (#34)#40

Merged
mmcky merged 3 commits into
mainfrom
feat/book-proof-counter-share
May 22, 2026
Merged

feat(book): shared counters across proof:* kinds (#34)#40
mmcky merged 3 commits into
mainfrom
feat/book-proof-counter-share

Conversation

@mmcky
Copy link
Copy Markdown

@mmcky mmcky commented May 22, 2026

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.

numbering:
  book: true
  proof:
    scope: section
  proof:lemma:       { counter: theorem }
  proof:proposition: { counter: theorem }
  proof:corollary:   { counter: theorem }
Identifier LaTeX PDF Before (qe-v5) After (this PR)
t-nsl Theorem 1.2.1 Theorem 1.2.1 ✓ Theorem 1.2.1 ✓
l-rsnb Lemma 1.2.2 Lemma 1.2.1 (−1) Lemma 1.2.2 ✓
t-bfpt Theorem 1.2.3 Theorem 1.2.2 (−1) Theorem 1.2.3 ✓
l-rxrn Lemma 1.2.4 Lemma 1.2.2 (−2) Lemma 1.2.4 ✓

Design — Tier 1 only

Per the spec in #34 (and the review at #34#issuecomment-4514310125):

  • Slot-owner-wins for counter mechanics: start, format, continue, reset_on_part, scope are read from the slot owner. Validator warns and drops those fields when set on an aliased kind.
  • Per-kind for rendering: label, template, and the enumerator wrap are unchanged so an aliased lemma still reads "Lemma 1.2.2" while sharing the theorem counter slot.
  • Transitive resolution at map-build time: chains like a→b→c flatten to a→c, b→c. No recursion in the hot path.
  • Cycle detection + warning, falls back to per-kind for cycle members.
  • Family-restricted: alias targets must be in the proof family; cross-family aliases emit a warning and are ignored. Bare names (counter: theorem) within the proof family normalize to the fully-qualified form (proof:theorem).
  • Builds the resolved-kind map locally in enumerate.ts (parallel to headingFormats) — keeps the validator pure and NumberingItem clean.

Tier 2 (shared_counters: list sugar) deferred — see issue body and the spec-tightening comment.

Where it lives

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

  • Existing numbering tests pass (88 in enumerate.spec.ts, 6 in numbering.spec.ts)
  • New: book-dp1 table from issue body pinned verbatim (t-nsl→1.2.1t-hartgrob→2.1.3)
  • New: aliased kind keeps own enumerator wrap (rendering stays per-kind)
  • New: shared slot resets together on scope change (regression guard for double resets)
  • New: transitive resolution a→b→c flattens correctly
  • New: cycle a→b→a warns + falls back to per-kind
  • New: cross-family alias warns + ignored
  • New (validator): bare → fully-qualified normalization, owner-field drops, label/template preserved
  • Full npm test green (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

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>
Copilot AI review requested due to automatic review settings May 22, 2026 02:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>.counter in frontmatter types + validation, including normalization of bare proof targets (e.g. theoremproof: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.

Comment on lines +397 to +412
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];
}
}
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +64 to +69
* 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.
*/
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

mmcky and others added 2 commits May 22, 2026 13:25
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>
@mmcky
Copy link
Copy Markdown
Author

mmcky commented May 22, 2026

Smoke test — book-dp1 end-to-end ✅

Cloned book-dp1 (mystmd-conversion branch) to a fresh /tmp checkout, added the three counter: aliases to its myst.yml, built with this branch's mystmd binary, and inspected the rendered output against the LaTeX PDF reference in the issue body.

Enumerators on the proof nodes match the LaTeX PDF verbatim:

Identifier LaTeX PDF Before (qe-v5) This PR
t-nsl Theorem 1.2.1 Theorem 1.2.1 ✓ Theorem 1.2.1 ✓
l-rsnb Lemma 1.2.2 Lemma 1.2.1 (−1) Lemma 1.2.2 ✓
t-bfpt Theorem 1.2.3 Theorem 1.2.2 (−1) Theorem 1.2.3 ✓
l-rxrn Lemma 1.2.4 Lemma 1.2.2 (−2) Lemma 1.2.4 ✓
p-iccm Proposition 2.1.1 Proposition 2.1.1 ✓ Proposition 2.1.1 ✓
t-hartgrob Theorem 2.1.3 Theorem 2.1.1 (−2) Theorem 2.1.3 ✓

Resolved cross-references ({prf:ref}\l-rsnb`etc.) render with both the corrected enumerator and the correct per-kind label — every occurrence ofLemma 1.2.2, Theorem 1.2.3, Lemma 1.2.4` reads as expected.

No regressions: proof:algorithm keeps chapter scope (algo-intro-auto-1 → 1.1), exercises keep their independent counter (ex-intro-auto-1..14 → 1.1..1.14), no new warnings in the build output.

Ready to merge.

@mmcky mmcky merged commit a63d53f into main May 22, 2026
8 of 10 checks passed
mmcky added a commit that referenced this pull request May 22, 2026
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>
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.

Book mode: shared counters across proof:* kinds (LaTeX \newtheorem[<other>] equivalent)

2 participants