** This file was created with an agent. **
Status: Open — design conversation concluded 2026-10-12, captured here as the base for a formal RPI cycle. Approach (b) is IMPLEMENTED (
6dbbf38e); approach (a) is the cycle’s remaining scope — research round COMPLETE 2026-10-12 (see RPI kick-off step 2). File references are as of499be3bd(pre-implementation) unless noted. Preceded by the structure-marks cycle (docs/planning/agentic-artefacts/thoughts/{research,plan}/2026-10-11-*, implemented in77d81198–788853d8) and the sup-marks round (84861807+ editor fixes499be3bd).
scripts/puppeteer-probes/qa-structure-marks.mjs
(viewer) and qa-editor-mark-styling.mjs (editor), both pass on the
live en/Typography load.Observation from the live Wikipedia article rendering (references list,
li-inline textblocks containing generic-inline / reproduce-as-inline
chains — e.g. li > span[mw-cite-backlink] > … > cite > a > span):
[data-node-type] inline element gets full block nodeProperties
writes: width: 455pt, text-align, block margins. Currently
layout-inert (display: inline survives; the display: table hack
in lib/css/shell/type-stage.css:219-243 is commented out), but
wrong.cite, bdi, reproduce-as-inline) have no nodeSpecToTypeSpec
entries, so their scopes resolve via the root fallback: an
18pt li (effective font-size via its typeSpec chain) contains
cite content explicitly written at 14pt (root baseFontSize)
— the inline node’s explicit write cascades over the textblock’s
typographic context. This is the same per-parent-context problem
that motivated the structure-marks cycle, now for inline nodes.viewer.typeroof.jsx:1142 (attach mapping) and :649 (constructor)
build the run’s composition subscription as
composition@${metaInfo.rootPath.parent.parent} with the explicit
assumption “The run’s path is <textblock>/content/<index>”.<textblock>/content/0/content/1) resolves parent.parent to the
inline node — which never publishes a composition: only
_isTextblock elements compose (document-nodes-meta/index.mjs:830:
innerHtml === null && !inInlineContext && !!childrenInInlineContext;
inline nodes are themselves in inline context)._buildInlineItems (composition-controller.ts:568-619) recurses
inlineContainer items; the nested leaves are in the payload with
their paths. Only the applicator never receives it: nested runs
observe a dead id and render OFF-MODE forever.li > backlink-atom + reference-text-node) composes
visually not at all.baseFontSize and relativeFontSize are single inherited scalar
properties (registered-properties.mjs:314-331, inherit: true,
defaults 12pt / 1). Inheritance is value shadowing: a child scope
that sets its own relativeFontSize replaces the parent’s
factor — it does not multiply onto the parent’s effective size.baseFontSize × relativeFontSize
(type-spec/fundamentals.mjs:91), and consumers read the resolved
GENERIC fontSize (e.g. per-run measurement:
composition-controller.ts:917, properties.get(GENERIC fontSize)).baseFontSize: 14 set at the root rides the whole tree; every
level’s relativeFontSize replaces rather than composes.relativeFontSize (e.g. bold: 2) computes from the
root base (28pt), not from the containing textblock’s
effective size (18pt × 2 = 36pt) — “marks are in a good spot [the
scope resolves at the textblock] … however, to be truly useful,
their baseFontSize === 1 should probably be the full font-size
of their parent typeSpec, which isn’t the case” (operator).fontSize write lands at the root’s
14pt regardless of the containing textblock’s 18pt context — see
observation 3.emAfter margin
machinery proves cross-scope resolution is already wired (below).6dbbf38e)Thread the enclosing textblock path through the renderer walk instead of deriving it by path arithmetic:
document-nodes-meta/index.mjs) knows when it
descends from a _isTextblock element; its childrenContext (the
{...context} spread — see derivations.mjs:277-289,
resolveElementRenderingPlan) gains a textblockRootPath field,
set at the textblock boundary and passed through unchanged for
deeper inline levels.viewer.typeroof.jsx:1142, :649) use
metaInfo.context.textblockRootPath ?? parent.parent (fallback =
today’s behavior) to build the composition@ id.Effect: nested inline content composes; the textblock’s rendered lines match the measured geometry; the references list stops overflowing. No controller change (it measures correctly today); no CSS change.
Status: implemented 2026-10-12 in 6dbbf38e — textblock element
provisioning threads textblockRootPath into the children context
(document-nodes-meta/index.mjs), both viewer sites use it with the
parent.parent fallback (viewer.typeroof.jsx), and
qa-structure-marks gained the nested-composition regression
(live: 152 of 153 nested inline nodes in the references list carry
composition-line spans; the delta is an atom-only node). The (a)
layers below remain open for the RPI cycle.
The nodeProperties generator skips the block-layout writes (width,
text-align, block margins) when the node spec is inline
(nodeSpec.get("inline") / inlineContent context). Font-size stays
(an inline node may deliberately set one) — this only removes the
inert-but-wrong block baggage.
Operator comment: This, to me, suggests to branch within update in
UIDocumentTypeSpecStyler. If feasible: this could be a chance to restructure
that implementation for readability.
Decided 2026-10-12 (operator): the skip is UNCONDITIONAL for pure
inline nodes — TypeRoof has no “inline-block” concept; an atom’s
width comes from the measurement/composition channel, not a CSS width
write (inert on display: inline anyway). A future intrinsically-sized
atom would be a new spec flag (a new concept), not a reason to keep
dead writes. Font-size stays as scoped above.
Precedent (existing, verified): the leading dimension’s values
already carry their basis as a unit —
type-spec/models.mjs:157: 'lineHeight', 'em', 'baseEm', 'lineHeightAfter', 'emAfter' — where em resolves against the scope’s
effective font-size (baseFontSize × relativeFontSize,
fundamentals.mjs:91) and baseEm against the root base. The proposal
widens this existing vocabulary to the font-size dimension itself
instead of inventing new machinery.
relativeFontSize as a unit-carrying value:
| unit | semantics | migration/impact |
|---|---|---|
baseEm |
× root baseFontSize — today’s behavior |
migration default for plain numbers: every tuned state keeps its exact meaning (non-breaking) |
em |
× the containing context’s effective font-size | style patches ONLY — channel split, operator 2026-10-12: NOT a typeSpec unit (see the note under the table) |
pt |
absolute effective size | captions/anchors pinned regardless of context |
Convention over machinery: the node-kind distinction becomes a
state-level convention — textblock typeSpecs (nodes with inline
content) write their fonts in No
“inline vs block children” special-casing in the resolution engine;
the units express the intent per value, exactly as the margins do.em (their inline content inherits the
parent’s effective size as basis); block containers write theirs in
baseEm/pt (the subtree scales from the base as today).
Channel split — Decided 2026-10-12 (operator): em is NOT a
typeSpec unit — the typeSpec vocabulary is baseEm (anchored basis)
em exists ONLY in the style-patch variant
(RelativeFontSizePatchUnitModel), where it resolves against the
containing context in the MERGE channel (phase 2). In the typeSpec
channel baseEm was already the only basis — so em there would
have been an alias, not a capability. The mid-tree “compose onto the
parent’s effective size” that the struck-through sentence wanted for
textblock scopes is covered by the KEPT mid-tree baseFontSize
re-anchoring (sub-document descriptions) instead.Operator question: do we need to rename “relativeFontSize” to a better fitting name?
Decided 2026-10-12 (operator): pair form {value, unit} per the
InlineLengthValueModel precedent (ManualHorizontalLayoutModel /
inlineMargins, type-spec/horizontal-layout-models.mjs) — re-use
or copy that model; plain numbers/strings parse as baseEm
(non-breaking migration default). Sub-question from the same exchange —
Decided (operator): KEEP baseFontSize as a first-class feature:
mid-tree values + baseEm relativeFontSize enable entire
sub-document descriptions (re-anchoring a subtree) within the same
typeSpec. No dissolution, no root-only restriction.
Rename — Decided 2026-10-12 (operator): KEEP the key
relativeFontSize (pair form under the same key; zero migration
cost, all states + downloads keep parsing via the baseEm default).
fontSizeSpec rejected as annoying; fontSize would be the best
name but is taken by the processed/synthetic property (research §1:
purely synthetic, no model field). UI label polish may follow post-cycle.
Slot-in Decided + implemented 2026-10-12 (operator): NO
baseFontSize field on style patches — patches are leafs (no
inheritance, no re-anchoring; absolute sizes go via the pair’s absolute
units; em/baseEm compose against the containing context). The pair
transport moved to relativeFontSizePairGen (shared); the merged scope
sources the base from the ORIGIN typeSpec. Plan phase 2b.
Pivot — Decided 2026-10-12 (operator, design conversation): treat
inline nodes like MARKS via a third style-link family
inlineNodesStyleLinks (next to intent/markStyleLinks): a third
StyleLinksMeta provisioning + map/prefix, and inline nodes route to
UIDocumentStyleStyler (the mark styler) in both renderers — its
write surface (font-family, font-size, features/variations, colors,
direction, lang) IS the a1 property surface, so the “branch within
update” shrinks to routing. α (typeSpec-channel parent-basis
synthetics) is REJECTED: generic/fontSize per-scope effective is
already the true em value; a parallel parent synthetic would fork the
basis (paradigm shift in the wrong direction). β (merge-channel basis)
is right for style patches — and thus for inline nodes:
createPatched resolves against the origin typeSpec’s outbound
effective size (scope-resolution.mjs :462-483). Inline-node
handling moves LAST in the phase plan (marks/patches verified first).
One-element inline-node DOM assessed FEASIBLE: PM natively supports
contentDOM === dom (doc desc + all marks are that case;
prosemirror-view dist :1519/:1234), the viewer already runs
single-element, outfitter widgets already gate on outer !== inner
(parameters display / labels stay inactive for inline — operator:
inline nodes need neither), two CSS selectors to consolidate.
Basis threading — the nodeProperties channel (the emAfter
precedent):
resolveNextNodePropertiesId
(document-nodes-meta/derivations.mjs:330-345) computes the next
sibling’s nodeProperties@<documentNodePath> registration id,
with the design comment: “The sibling’s style facts (fontSize via
its typeSpec layer, leading via the layout keys) live in its
per-document-node node-properties scope; the typeSpec-based lookup
this replaces could not resolve the layout keys.”nsProps.get(GENERIC fontSize)
(prosemirror/type-spec.typeroof.jsx:535+; viewer counterpart at
viewer.typeroof.jsx:534), wired as nextNodeProperties@ in both
renderers.document-nodes-meta/index.mjs, parentNodeProperties), and
rootNodeProperties@ seeds the chain.Architectural consequence: this dissolves the original a2 problem
— inline nodes stop needing nodeSpecToTypeSpec links (combinatorial
per-parent bookkeeping, as rejected at the start of the
structure-marks cycle) and become readers of the node-scope chain:
their font basis resolves in the containing node’s scope, not the root
fallback. The typeSpec channel stays what it is — the tree of
per-block typographic contexts whose synthesized fontSize feeds the
node scopes (“fontSize via its typeSpec layer”).
Three layers, one architecture:
baseEm (non-breaking); emem — implemented 2026-10-12 (approach (a)
phase 1+2: pair form + merge-channel em basis via the
CONTAINING_FONT_SIZE alias in PatchedScopeProperties; plan
thoughts/plans/2026-10-12-2127-…, commits abcf36f0 + phase-2
commit).emAfter precedent): inline-node and mark/patch scopes resolve
their font basis against the containing node’s scope.fontSize synthesis feeds the node scopes.LeadingRunion’s en-compensation
(type-spec/models.mjs:269-272:
actuallineLength = lineLengthEn / relativeFontSize) — arithmetic
assumes a plain factor; needs re-derivation per unit.em/baseEm/en consumers (line-length, gutters, paddings,
margins) must be inventoried for which basis they assumed.baseFontSize withers to a root-only anchor (pt) once
mid-tree scopes are em-based.derivations.mjs
getStyleLinkPropertiesId; whether their em values resolve
against the containing node’s scope needs mapping).Recommended sequence (operator’s call):
6dbbf38e): dispatcher context threading + the two viewer sites +
qa-structure-marks probe extension for nested-run composition,
live-verified on Typography./research_codebase cycle on font-size resolution & re-basing:
(i) where fontSize/baseFontSize/relativeFontSize are
synthesized and consumed (registered-properties, fundamentals,
node-properties-generators, composition measurement);
(ii) the nodeProperties scope composition (how the typeSpec layer
lands in each node scope; parent/root threading);
(iii) every em/baseEm/en resolution site (margins,
LeadingRunion, leading, line-length);
(iv) the inline-node + patch scope basis today (root fallbacks,
mark style-link resolution contexts);
(v) the unit vocabulary decision: plain number → baseEm
migration, em semantics, node-em lookup design against the
containing node’s scope.
DONE 2026-10-12 — research doc:
thoughts/research/2026-10-12-1723-font-size-resolution-and-rebasing.md
(RPI convention: cycle docs live in thoughts/ at project root during
the cycle and archive to
docs/planning/agentic-artefacts/thoughts/ when the plan concludes —
the earlier sketch of an output location inside this directory was an
oversight, per operator 2026-10-12).
Key resolutions for the plan: (a) fontSize is a pure synthetic
(base × relative) and every consumer receives pre-flattened plain
numbers — no unit stripping exists anywhere; (b) the nodeProperties
parent layer demonstrably carries the containing node’s
generic/fontSize (the emAfter sibling lookup reads it), so
node-em is addressable today; © patch scopes have no parent
(baseFontSize falls to the model default) — the patch em basis
must be pinned to the merged style-link scope that measurement
reads; (d) no em-intent values exist in states/fixtures — the
plain-number → baseEm migration is non-breaking at document
scope; (e) inline nodes have no dedicated font-size machinery —
the empty nodeSpecToTypeSpec link is origin-anchored and walks up
to the root. Next: /create_plan for the implementation phases
(likely: generator skip a1; units + migration; node-em threading;
inline-node scope re-resolution away from root fallback; states
re-tuning).npm run perf:composition, which stays OFF LIMITS per the
operator rule 2026-10-06)./create_plan cycle — DONE 2026-10-12 — plan doc:
thoughts/plans/2026-10-12-2127-inline-node-refinements-approach-a.md
(six phases: pair form → merge-channel em → one-element inline
DOM → inlineNodesStyleLinks + routing → “Node Style Links” UI
wiring → composition basis threading + QA/re-tuning). Next:
/implement_plan.abcf36f0), 2
(0524e3d6), 2b (c2b42282), 3 (0d012dd4) committed; phase 4 in
the working tree (family + routing, reconstructed clean; 541 vitest
green; probe steps 1–5 pass). One open failure — fresh viewer
attach after a mode switch leaves inline elements unstyled →
handed over to a sub-RPI cycle; handover doc:
docs/planning/inline-node-refinements/2026-10-13-phase4-viewer-channel-HANDOVER.md.6394787b + fresh-attach fix
4532ae22 (sub-cycle docs archived); phase 5 “Node Style Links” UI
31107d2a; phase 6 9ad62e29 (nested marks anchor at the
containing typeSpec — getResolvablePathOfTypes at all four
rendered channels) + a014ceaf (the measured-side fallback:
CompositionSource.resolveInlineStyleLink, innermost-wins
threading, explicit-everything contract) + d5b98d0b (the quote →
serif italic demo edge in the wikipedia state). The cycle’s visible
goal verified live: nested italic {1, em} at the li context
(18pt/24px) in both renderers; quotes at AmstelvarA2 Italic 18pt;
references list composed without overflow. Open follow-up: the
manual-wake refactor (below)._widgets membership for the
inline wrappers (plan
thoughts/plans/2026-10-13-1616-inline-styler-fresh-attach-membership.md,
research thoughts/research/2026-10-13-1503-viewer-fresh-attach-inline-styler.md).
The hand-rolled _createWidget + changedMap + widget.update wake
sites (viewer.typeroof.jsx _syncMetaInfo dispatcher — which also
serves the block channel — and _inlineStyleTriggerUpdate) are
deliberately NOT refactored yet. A follow-up cycle may evaluate full
framework reliance (drop the manual blocks; requires verifying that a
cascade always follows a _syncMetaInfo re-provision) with a shared
helper as fallback. See the plan’s “Follow-up Note”.docs/planning/agentic-artefacts/thoughts/research/2026-10-11-1300-structure-marks-for-nested-combinable-chains.md,
docs/planning/agentic-artefacts/thoughts/plan/2026-10-11-1355-structure-marks-nested-combinables.md.84861807 (sup citations as marks),
bd129521 (tests), 499be3bd (editor mark subscriptions: nested
marks, declarations, bag leak).docs/planning/type-spec-resolution/ (order 49) —
relative typeSpec linking & silent typeSpecs.scripts/puppeteer-probes/qa-structure-marks.mjs,
qa-editor-mark-styling.mjs; runner
scripts/debug-with-puppeteer.mjs.width ever legitimately apply to inline nodes (e.g.
reproduce-as-inline atoms), or is the a1 skip unconditional for
inline specs?relativeFontSize values re-interpret under em
(the states’ headings chain: h1 4.5714 → multiplicative)?
Breaking-change review, level by level.
— Answered (research 2026-10-12, §5): none; all stored values
are plain-number baseEm-equivalents.em resolves in the merge channel against the origin outbound
effective size (see the pivot note under layer a2).em against the containing node’s scope (the li’s
18pt) or the typeSpec layer’s effective size — pin the resolved
basis the composition already measures against
(composition-controller.ts:917).
— Answered 2026-10-12: merge channel (createPatched, origin
outbound effective size) — the same merged scope measurement reads;
see the pivot note under layer a2.text-composition suite vehicles (setPatchRelativeFontSize
asserts doubling), probe expectations.
— Mapped (research 2026-10-12, §5): all fixtures plain-number
strings; probes carry no numeric font-size expectations; concrete
test surface: the text-composition doubling/clear-restore contracts
and four hyphenation-suite baseFontSize overrides.