** This file was created with an agent. **
2026-08-24-null-typespec-analysis_Fable-5.md
(rounds 1–6; round 6 pins the source state at 2f6ac4b2 +
comment-only dirt).~/MANNERS.md, COLLABORATION.md —
including the commit-message metadata block: model, provider,
agent version read from their actual sources).npm run typecheck && npm run lint, plus the test
suite. npm run format:write before presenting a diff.Characterization tests come first (round 6: the resolver has zero test coverage — no refactoring without a net). Then model flags (inert until read), then resolver changes behind the flags, then provisioning, then guards/UI. Every step leaves the tree green and shippable; feature-visible behavior switches on only when the flag/link is actually used, so no step breaks existing documents.
The tree carries two uncommitted comment-only changes
(integration.typeroof.jsx typo, type-spec-models.mjs note) and the
untracked docs/planning/type-spec-resolution/. Decide whether to
commit these first so the feature series starts clean. Recommended:
one [docs]/[minor] commit.
New file lib/js/components/prosemirror/type-spec-resolution.test.mjs
(harness patterns from integration.test.mjs). Behaviors to pin, as
they are today:
..-injection attempt resolves to
root — the isRootOf guard);children interposition
behavior (pin it, including the arguably-broken
origin/children/a/b shape — it is current behavior);getTypeSpecsMethod: selection spanning two node types yields both
resolved typeSpecs.No production code changes. Commit 1.
The round-3 helper: logical link segments (../hr, quote/paragraph)
↔ storage path (children-interleaved), .. consuming one logical
level (= a children/{key} pair). Location: alongside the resolver in
integration.typeroof.jsx (or a small new module if it stays
UI-consumable — decide at review). Pure functions, no side effects
(CODINGSTYLE). Behavior tests: round-trips, .. consumption, out-of-
bounds .. reported (not clamped), rejection/pass-through of
malformed input. Not yet wired into the resolver. Commit 2.
excludeFromFallback and noStyler, BooleanModel, default false
(type-spec-models.mjs, TypeSpecModel — round-6 model table).
Check for an existing boolean-with-default model pattern before
creating anything new (CODINGSTYLE: search before creating). Inert:
nothing reads them yet. Behavior test: (de)serialization round-trip of
a typeSpec tree with flags set; existing documents load unchanged.
UI checkboxes are not in this step. Commit 3.
excludeFromFallback in the walk_getBestTypeSpecPropertiesId: during the fallback walk (not for
explicit link hits — round-2 row 2 semantics), skip typeSpecs whose
excludeFromFallback is true. Requires access to the typeSpec
entries, not only the registry — mind that the memo cache key already
covers nodeSpecToTypeSpec but not typeSpec flag changes; the
registered-membership re-validation on cache hits is the existing
correctness valve — verify it suffices for flag flips, otherwise the
cache needs the typeSpec instance in its key. Behavior tests:
noStyler, if set).
Commit 4.noStyler, PM sideRound-5 principle: resolution is untouched; only styler provisioning consults the flag on the resolved spec.
TypeSpecSubscriptions/UIDocumentNodeOutfitter: when the resolved
typeSpec has noStyler, do not provision UIDocumentTypeSpecStyler
(subscription itself remains — marks need their parent; Q1/d1 means
mark resolution is unchanged).UIDocumentTypeSpecStyler a
destroy() that removes every inline style/CSS property it sets
(round-4 item 2) — needed for styled→silent transitions.noStyler, Viewer side + dynamic flagUIDocumentElement: _hasTypeSpecStyling keeps meaning “a spec is
resolvable at all” and stays true for silent nodes (round-5
narrowing); _provisionTypeSpecStyler additionally checks noStyler
on the resolved spec.nextProperties@: Q2© — the silent sibling’s resolved spec is
used as margin reference; expected to need no code change
(resolver returns the path regardless); cover with a behavior test:
a styled node’s after-margin against a silent next sibling uses the
sibling’s spec properties._prosemirrorDispatchTransaction (integration.typeroof.jsx:1268):
defensive guard for an empty getTypeSpecs result (Q3(a) makes it
unreachable via silence; guard against the genuinely-empty case)._typeSpecFullLabel, fundamentals) as nice-to-have.Round 3, in getTypeSpecPropertiesIdMethod:
isExplicitlyRelative || parts[0] === Path.PARENT →
parent-anchored; all else origin-anchored (unchanged);pathOfTypes.slice(0, -1); document root’s parent = origin;..-pair consumption before any
append (the sanitize trap);.. → broken link → normal fallback walk;Behavior tests: ./h3 under a mapped parent; ../hr sibling;
unmapped parent → anchor degrades to root → ./h3 behaves like /h3;
out-of-bounds ..; deep nesting; flags × relative links (silent
anchor still anchors — round-3 revision). Commit 8.
type-spec-properties.typeroof.jsx / TYPESPEC_PPS_MAP route —
check how existing booleans are wired there).UIStyleNodeToTypeSpecValueLabel (it currently resolves the raw
path against the root — relative links will display as broken
without a resolution hook; at minimum show the raw link string
distinctly).
Commit 9 (possibly split UI-per-surface).The round-5 documentation requirement (“a silent spec doesn’t style its node but still governs its marks” — feature, not leak; the mark-rendering palette table). Where user docs live is to be decided with the operator. Update the design document’s register. Commit 10.
TypeStagePaneStyler changes (round 4: separate surface, noted,
untouched).| risk | mitigation |
|---|---|
| memo cache vs. typeSpec flag flips (Step 4) | verify re-validation valve; else extend cache key — decide at Step 4 review |
| viewer dynamism explodes (Step 6) | split commit: static-correct first |
| relative-link recursion meets unknown-type siblings (viewer “parity edge case” TODO) | pin current behavior in Step 1; do not fix silently |
| tests bind to internals under pressure | working-agreement §3; reviewer rejects such tests |